diff --git a/pdf-umbenenner-coverage/pom.xml b/pdf-umbenenner-coverage/pom.xml
index 92be938..76c1ca9 100644
--- a/pdf-umbenenner-coverage/pom.xml
+++ b/pdf-umbenenner-coverage/pom.xml
@@ -74,6 +74,16 @@
+ * Verifies immutability, value semantics, and correct handling of raw AI responses.
+ */
+class AiRawResponseTest {
+
+ private static final String VALID_JSON_RESPONSE = "{\"date\": \"2026-03-05\", \"title\": \"Stromabrechnung\", \"reasoning\": \"...\"}";
+ private static final String MALFORMED_JSON = "{invalid json}";
+ private static final String EMPTY_RESPONSE = "";
+ private static final String WHITESPACE_RESPONSE = " ";
+
+ @Test
+ void constructor_createsAiRawResponseWithValidContent() {
+ AiRawResponse response = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertNotNull(response);
+ assertEquals(VALID_JSON_RESPONSE, response.content());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenContentIsNull() {
+ assertThrows(NullPointerException.class, () -> new AiRawResponse(null),
+ "Constructor should throw NullPointerException for null content");
+ }
+
+ @Test
+ void constructor_acceptsEmptyContent() {
+ AiRawResponse response = new AiRawResponse(EMPTY_RESPONSE);
+ assertEquals(EMPTY_RESPONSE, response.content());
+ }
+
+ @Test
+ void constructor_acceptsMalformedJsonContent() {
+ AiRawResponse response = new AiRawResponse(MALFORMED_JSON);
+ assertEquals(MALFORMED_JSON, response.content());
+ }
+
+ @Test
+ void constructor_acceptsWhitespaceOnlyContent() {
+ AiRawResponse response = new AiRawResponse(WHITESPACE_RESPONSE);
+ assertEquals(WHITESPACE_RESPONSE, response.content());
+ }
+
+ @Test
+ void content_returnsTheConstructorValue() {
+ AiRawResponse response = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertEquals(VALID_JSON_RESPONSE, response.content());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalContent() {
+ AiRawResponse response1 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response2 = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertEquals(response1, response2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentContent() {
+ AiRawResponse response1 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response2 = new AiRawResponse(MALFORMED_JSON);
+ assertNotEquals(response1, response2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ AiRawResponse response = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertNotEquals(null, response);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ AiRawResponse response = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertNotEquals(VALID_JSON_RESPONSE, response);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalContent() {
+ AiRawResponse response1 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response2 = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertEquals(response1.hashCode(), response2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentContent() {
+ AiRawResponse response1 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response2 = new AiRawResponse(MALFORMED_JSON);
+ assertNotEquals(response1.hashCode(), response2.hashCode());
+ }
+
+ @Test
+ void toString_containsTheContent() {
+ AiRawResponse response = new AiRawResponse(VALID_JSON_RESPONSE);
+ assertTrue(response.toString().contains(VALID_JSON_RESPONSE));
+ }
+
+ @Test
+ void aiRawResponseCanBeUsedAsMapKey() {
+ AiRawResponse response1 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response2 = new AiRawResponse(VALID_JSON_RESPONSE);
+ AiRawResponse response3 = new AiRawResponse(MALFORMED_JSON);
+
+ var map = new java.util.HashMap
+ * Verifies immutability, value semantics, validation, and correct behavior
+ * as the stable identity for a document based on its content hash.
+ */
+class DocumentFingerprintTest {
+
+ private static final String VALID_SHA256 = "0000000000000000000000000000000000000000000000000000000000000000";
+ private static final String VALID_SHA256_2 = "a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3";
+ private static final String VALID_SHA256_3 = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
+
+ @Test
+ void constructor_createsDocumentFingerprintWithValidSha256() {
+ DocumentFingerprint fingerprint = new DocumentFingerprint(VALID_SHA256);
+ assertNotNull(fingerprint);
+ assertEquals(VALID_SHA256, fingerprint.sha256Hex());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenSha256IsNull() {
+ assertThrows(NullPointerException.class, () -> new DocumentFingerprint(null),
+ "Constructor should throw NullPointerException for null sha256Hex");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenSha256IsTooShort() {
+ String tooShort = "356a192b7913b04c54574d18c28d46e6395428a"; // 39 chars
+ assertThrows(IllegalArgumentException.class, () -> new DocumentFingerprint(tooShort),
+ "Constructor should throw IllegalArgumentException for sha256Hex that is too short");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenSha256IsTooLong() {
+ String tooLong = "356a192b7913b04c54574d18c28d46e6395428abXX"; // 66 chars
+ assertThrows(IllegalArgumentException.class, () -> new DocumentFingerprint(tooLong),
+ "Constructor should throw IllegalArgumentException for sha256Hex that is too long");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenSha256ContainsInvalidCharacters() {
+ String invalid = "356a192b7913b04c54574d18c28d46e6395428ab" + "ZZZZZZZZZZZZZZZZZZZZZZ"; // Has uppercase letters
+ assertThrows(IllegalArgumentException.class, () -> new DocumentFingerprint(invalid),
+ "Constructor should throw IllegalArgumentException for sha256Hex with non-hex characters");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenSha256HasUppercaseLetters() {
+ String uppercase = "356A192B7913B04C54574D18C28D46E6395428AB";
+ assertThrows(IllegalArgumentException.class, () -> new DocumentFingerprint(uppercase),
+ "Constructor should throw IllegalArgumentException for sha256Hex with uppercase letters");
+ }
+
+ @Test
+ void constructor_acceptsValidLowercaseHexadecimal() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256_2);
+ DocumentFingerprint fingerprint3 = new DocumentFingerprint(VALID_SHA256_3);
+
+ assertEquals(VALID_SHA256, fingerprint1.sha256Hex());
+ assertEquals(VALID_SHA256_2, fingerprint2.sha256Hex());
+ assertEquals(VALID_SHA256_3, fingerprint3.sha256Hex());
+ }
+
+ @Test
+ void constructor_acceptsAllHexDigits() {
+ String allDigits = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+ DocumentFingerprint fingerprint = new DocumentFingerprint(allDigits);
+ assertEquals(allDigits, fingerprint.sha256Hex());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalSha256Values() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256);
+ assertEquals(fingerprint1, fingerprint2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentSha256Values() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256_2);
+ assertNotEquals(fingerprint1, fingerprint2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ DocumentFingerprint fingerprint = new DocumentFingerprint(VALID_SHA256);
+ assertNotEquals(null, fingerprint);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ DocumentFingerprint fingerprint = new DocumentFingerprint(VALID_SHA256);
+ assertNotEquals(VALID_SHA256, fingerprint);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalValues() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256);
+ assertEquals(fingerprint1.hashCode(), fingerprint2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentValues() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256_2);
+ assertNotEquals(fingerprint1.hashCode(), fingerprint2.hashCode());
+ }
+
+ @Test
+ void toString_containsTheSha256HexValue() {
+ DocumentFingerprint fingerprint = new DocumentFingerprint(VALID_SHA256);
+ assertTrue(fingerprint.toString().contains(VALID_SHA256));
+ }
+
+ @Test
+ void documentFingerprintCanBeUsedAsMapKey() {
+ DocumentFingerprint fingerprint1 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint2 = new DocumentFingerprint(VALID_SHA256);
+ DocumentFingerprint fingerprint3 = new DocumentFingerprint(VALID_SHA256_2);
+
+ var map = new java.util.HashMap
+ * Verifies immutability, value semantics, and correct validation of naming proposals.
+ */
+class NamingProposalTest {
+
+ private static final LocalDate SAMPLE_DATE = LocalDate.of(2026, 3, 31);
+ private static final LocalDate DIFFERENT_DATE = LocalDate.of(2025, 12, 25);
+ private static final String SAMPLE_TITLE = "Stromabrechnung";
+ private static final String DIFFERENT_TITLE = "Gasabrechnung";
+ private static final String SAMPLE_REASONING = "Found electricity bill dated 2026-03-31 in document text.";
+ private static final String EMPTY_REASONING = "";
+
+ @Test
+ void constructor_createsNamingProposalWithValidValues() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotNull(proposal);
+ assertEquals(SAMPLE_DATE, proposal.resolvedDate());
+ assertEquals(DateSource.AI_PROVIDED, proposal.dateSource());
+ assertEquals(SAMPLE_TITLE, proposal.validatedTitle());
+ assertEquals(SAMPLE_REASONING, proposal.aiReasoning());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenResolvedDateIsNull() {
+ assertThrows(NullPointerException.class,
+ () -> new NamingProposal(null, DateSource.AI_PROVIDED, SAMPLE_TITLE, SAMPLE_REASONING),
+ "Constructor should throw NullPointerException for null resolvedDate");
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenDateSourceIsNull() {
+ assertThrows(NullPointerException.class,
+ () -> new NamingProposal(SAMPLE_DATE, null, SAMPLE_TITLE, SAMPLE_REASONING),
+ "Constructor should throw NullPointerException for null dateSource");
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenValidatedTitleIsNull() {
+ assertThrows(NullPointerException.class,
+ () -> new NamingProposal(SAMPLE_DATE, DateSource.AI_PROVIDED, null, SAMPLE_REASONING),
+ "Constructor should throw NullPointerException for null validatedTitle");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenValidatedTitleIsEmpty() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new NamingProposal(SAMPLE_DATE, DateSource.AI_PROVIDED, "", SAMPLE_REASONING),
+ "Constructor should throw IllegalArgumentException for empty validatedTitle");
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenAiReasoningIsNull() {
+ assertThrows(NullPointerException.class,
+ () -> new NamingProposal(SAMPLE_DATE, DateSource.AI_PROVIDED, SAMPLE_TITLE, null),
+ "Constructor should throw NullPointerException for null aiReasoning");
+ }
+
+ @Test
+ void constructor_acceptsEmptyAiReasoning() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.FALLBACK_CURRENT,
+ SAMPLE_TITLE,
+ EMPTY_REASONING);
+
+ assertEquals(EMPTY_REASONING, proposal.aiReasoning());
+ }
+
+ @Test
+ void constructor_acceptsFallbackCurrentDateSource() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.FALLBACK_CURRENT,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(DateSource.FALLBACK_CURRENT, proposal.dateSource());
+ }
+
+ @Test
+ void resolvedDate_returnsTheConstructorValue() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(SAMPLE_DATE, proposal.resolvedDate());
+ }
+
+ @Test
+ void dateSource_returnsTheConstructorValue() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.FALLBACK_CURRENT,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(DateSource.FALLBACK_CURRENT, proposal.dateSource());
+ }
+
+ @Test
+ void validatedTitle_returnsTheConstructorValue() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(SAMPLE_TITLE, proposal.validatedTitle());
+ }
+
+ @Test
+ void aiReasoning_returnsTheConstructorValue() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(SAMPLE_REASONING, proposal.aiReasoning());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalValues() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(proposal1, proposal2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentDates() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ DIFFERENT_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(proposal1, proposal2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentDateSources() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.FALLBACK_CURRENT,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(proposal1, proposal2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentTitles() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ DIFFERENT_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(proposal1, proposal2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentReasoning() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ "Different reasoning");
+
+ assertNotEquals(proposal1, proposal2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(null, proposal);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(SAMPLE_TITLE, proposal);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalValues() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertEquals(proposal1.hashCode(), proposal2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentValues() {
+ NamingProposal proposal1 = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+ NamingProposal proposal2 = new NamingProposal(
+ DIFFERENT_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ assertNotEquals(proposal1.hashCode(), proposal2.hashCode());
+ }
+
+ @Test
+ void toString_containsAllFields() {
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ SAMPLE_TITLE,
+ SAMPLE_REASONING);
+
+ String str = proposal.toString();
+ assertNotNull(str);
+ assertFalse(str.isEmpty());
+ }
+
+ @Test
+ void constructor_acceptsTitleWithSpecialCharacters() {
+ String titleWithUmlauts = "Stromabrechnung Üben";
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ titleWithUmlauts,
+ SAMPLE_REASONING);
+
+ assertEquals(titleWithUmlauts, proposal.validatedTitle());
+ }
+
+ @Test
+ void constructor_acceptsTitleWithSpaces() {
+ String titleWithSpaces = "Rechnung für März";
+ NamingProposal proposal = new NamingProposal(
+ SAMPLE_DATE,
+ DateSource.AI_PROVIDED,
+ titleWithSpaces,
+ SAMPLE_REASONING);
+
+ assertEquals(titleWithSpaces, proposal.validatedTitle());
+ }
+}
diff --git a/pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/PromptIdentifierTest.java b/pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/PromptIdentifierTest.java
new file mode 100644
index 0000000..16a11f9
--- /dev/null
+++ b/pdf-umbenenner-domain/src/test/java/de/gecheckt/pdf/umbenenner/domain/model/PromptIdentifierTest.java
@@ -0,0 +1,142 @@
+package de.gecheckt.pdf.umbenenner.domain.model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for {@link PromptIdentifier} value object.
+ *
+ * Verifies immutability, value semantics, and correct handling of prompt identifiers.
+ */
+class PromptIdentifierTest {
+
+ private static final String VALID_IDENTIFIER_1 = "prompt_de_v1.txt";
+ private static final String VALID_IDENTIFIER_2 = "2026-03-v2";
+ private static final String VALID_IDENTIFIER_3 = "sha256:abc123def456";
+
+ @Test
+ void constructor_createsPromptIdentifierWithValidIdentifier() {
+ PromptIdentifier identifier = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertNotNull(identifier);
+ assertEquals(VALID_IDENTIFIER_1, identifier.identifier());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenIdentifierIsNull() {
+ assertThrows(NullPointerException.class, () -> new PromptIdentifier(null),
+ "Constructor should throw NullPointerException for null identifier");
+ }
+
+ @Test
+ void constructor_acceptsEmptyIdentifier() {
+ PromptIdentifier identifier = new PromptIdentifier("");
+ assertEquals("", identifier.identifier());
+ }
+
+ @Test
+ void constructor_acceptsVariousIdentifierFormats() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_2);
+ PromptIdentifier id3 = new PromptIdentifier(VALID_IDENTIFIER_3);
+
+ assertEquals(VALID_IDENTIFIER_1, id1.identifier());
+ assertEquals(VALID_IDENTIFIER_2, id2.identifier());
+ assertEquals(VALID_IDENTIFIER_3, id3.identifier());
+ }
+
+ @Test
+ void identifier_returnsTheConstructorValue() {
+ PromptIdentifier identifier = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertEquals(VALID_IDENTIFIER_1, identifier.identifier());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalIdentifiers() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertEquals(id1, id2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentIdentifiers() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_2);
+ assertNotEquals(id1, id2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ PromptIdentifier identifier = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertNotEquals(null, identifier);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ PromptIdentifier identifier = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertNotEquals(VALID_IDENTIFIER_1, identifier);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalIdentifiers() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertEquals(id1.hashCode(), id2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentIdentifiers() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_2);
+ assertNotEquals(id1.hashCode(), id2.hashCode());
+ }
+
+ @Test
+ void toString_containsTheIdentifier() {
+ PromptIdentifier identifier = new PromptIdentifier(VALID_IDENTIFIER_1);
+ assertTrue(identifier.toString().contains(VALID_IDENTIFIER_1));
+ }
+
+ @Test
+ void promptIdentifierCanBeUsedAsMapKey() {
+ PromptIdentifier id1 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id2 = new PromptIdentifier(VALID_IDENTIFIER_1);
+ PromptIdentifier id3 = new PromptIdentifier(VALID_IDENTIFIER_2);
+
+ var map = new java.util.HashMap
+ * Verifies immutability, value semantics, and correct validation of source document candidates.
+ */
+class SourceDocumentCandidateTest {
+
+ private static final String VALID_IDENTIFIER = "document.pdf";
+ private static final String VALID_IDENTIFIER_2 = "another_file.PDF";
+ private static final long VALID_FILE_SIZE = 1024L;
+ private static final long ZERO_FILE_SIZE = 0L;
+ private static final String VALID_LOCATOR_VALUE = "/home/user/docs/document.pdf";
+
+ @Test
+ void constructor_createsSourceDocumentCandidateWithValidValues() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertNotNull(candidate);
+ assertEquals(VALID_IDENTIFIER, candidate.uniqueIdentifier());
+ assertEquals(VALID_FILE_SIZE, candidate.fileSizeBytes());
+ assertEquals(locator, candidate.locator());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenUniqueIdentifierIsNull() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ assertThrows(NullPointerException.class,
+ () -> new SourceDocumentCandidate(null, VALID_FILE_SIZE, locator),
+ "Constructor should throw NullPointerException for null uniqueIdentifier");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenUniqueIdentifierIsEmpty() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ assertThrows(IllegalArgumentException.class,
+ () -> new SourceDocumentCandidate("", VALID_FILE_SIZE, locator),
+ "Constructor should throw IllegalArgumentException for empty uniqueIdentifier");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenFileSizeIsNegative() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ assertThrows(IllegalArgumentException.class,
+ () -> new SourceDocumentCandidate(VALID_IDENTIFIER, -1L, locator),
+ "Constructor should throw IllegalArgumentException for negative fileSizeBytes");
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenLocatorIsNull() {
+ assertThrows(NullPointerException.class,
+ () -> new SourceDocumentCandidate(VALID_IDENTIFIER, VALID_FILE_SIZE, null),
+ "Constructor should throw NullPointerException for null locator");
+ }
+
+ @Test
+ void constructor_acceptsZeroFileSize() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ ZERO_FILE_SIZE,
+ locator);
+
+ assertEquals(ZERO_FILE_SIZE, candidate.fileSizeBytes());
+ }
+
+ @Test
+ void constructor_acceptsVeryLargeFileSize() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ long largeSize = Long.MAX_VALUE;
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ largeSize,
+ locator);
+
+ assertEquals(largeSize, candidate.fileSizeBytes());
+ }
+
+ @Test
+ void uniqueIdentifier_returnsTheConstructorValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertEquals(VALID_IDENTIFIER, candidate.uniqueIdentifier());
+ }
+
+ @Test
+ void fileSizeBytes_returnsTheConstructorValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertEquals(VALID_FILE_SIZE, candidate.fileSizeBytes());
+ }
+
+ @Test
+ void locator_returnsTheConstructorValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertEquals(locator, candidate.locator());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalValues() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertEquals(candidate1, candidate2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentUniqueIdentifiers() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER_2,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertNotEquals(candidate1, candidate2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentFileSizes() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ 2048L,
+ locator);
+
+ assertNotEquals(candidate1, candidate2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentLocators() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator("/other/path/document.pdf");
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator1);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator2);
+
+ assertNotEquals(candidate1, candidate2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertNotEquals(null, candidate);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertNotEquals(VALID_IDENTIFIER, candidate);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalValues() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ assertEquals(candidate1.hashCode(), candidate2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentValues() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator("/other/path/document.pdf");
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator1);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator2);
+
+ assertNotEquals(candidate1.hashCode(), candidate2.hashCode());
+ }
+
+ @Test
+ void toString_containsAllFields() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentCandidate candidate = new SourceDocumentCandidate(
+ VALID_IDENTIFIER,
+ VALID_FILE_SIZE,
+ locator);
+
+ String str = candidate.toString();
+ assertNotNull(str);
+ assertFalse(str.isEmpty());
+ }
+
+ @Test
+ void sourceDocumentCandidateCanBeUsedInCollections() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR_VALUE);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator("/other/path/document.pdf");
+
+ SourceDocumentCandidate candidate1 = new SourceDocumentCandidate(VALID_IDENTIFIER, VALID_FILE_SIZE, locator1);
+ SourceDocumentCandidate candidate2 = new SourceDocumentCandidate(VALID_IDENTIFIER, VALID_FILE_SIZE, locator1);
+ SourceDocumentCandidate candidate3 = new SourceDocumentCandidate(VALID_IDENTIFIER_2, VALID_FILE_SIZE, locator2);
+
+ var set = new java.util.HashSet
+ * Verifies immutability, value semantics, and validation of the opaque document locator.
+ */
+class SourceDocumentLocatorTest {
+
+ private static final String VALID_LOCATOR = "C:\\Users\\test\\documents\\file.pdf";
+ private static final String VALID_LOCATOR_2 = "/home/user/documents/another.pdf";
+ private static final String VALID_LOCATOR_3 = "relative/path/to/document.pdf";
+
+ @Test
+ void constructor_createsSourceDocumentLocatorWithValidValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR);
+ assertNotNull(locator);
+ assertEquals(VALID_LOCATOR, locator.value());
+ }
+
+ @Test
+ void constructor_throwsNullPointerExceptionWhenValueIsNull() {
+ assertThrows(NullPointerException.class, () -> new SourceDocumentLocator(null),
+ "Constructor should throw NullPointerException for null value");
+ }
+
+ @Test
+ void constructor_throwsIllegalArgumentExceptionWhenValueIsEmpty() {
+ assertThrows(IllegalArgumentException.class, () -> new SourceDocumentLocator(""),
+ "Constructor should throw IllegalArgumentException for empty value");
+ }
+
+ @Test
+ void constructor_acceptsValidPaths() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR_2);
+ SourceDocumentLocator locator3 = new SourceDocumentLocator(VALID_LOCATOR_3);
+
+ assertEquals(VALID_LOCATOR, locator1.value());
+ assertEquals(VALID_LOCATOR_2, locator2.value());
+ assertEquals(VALID_LOCATOR_3, locator3.value());
+ }
+
+ @Test
+ void constructor_acceptsOpaqueValue() {
+ // The locator is intentionally opaque - it can contain any non-empty string
+ String opaqueValue = "adapter-internal-encoding:12345:abcdef";
+ SourceDocumentLocator locator = new SourceDocumentLocator(opaqueValue);
+ assertEquals(opaqueValue, locator.value());
+ }
+
+ @Test
+ void value_returnsTheConstructorValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR);
+ assertEquals(VALID_LOCATOR, locator.value());
+ }
+
+ @Test
+ void equals_returnsTrueForIdenticalValues() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR);
+ assertEquals(locator1, locator2);
+ }
+
+ @Test
+ void equals_returnsFalseForDifferentValues() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR_2);
+ assertNotEquals(locator1, locator2);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithNull() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR);
+ assertNotEquals(null, locator);
+ }
+
+ @Test
+ void equals_returnsFalseWhenComparedWithDifferentType() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR);
+ assertNotEquals(VALID_LOCATOR, locator);
+ }
+
+ @Test
+ void hashCode_isSameForIdenticalValues() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR);
+ assertEquals(locator1.hashCode(), locator2.hashCode());
+ }
+
+ @Test
+ void hashCode_isDifferentForDifferentValues() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR_2);
+ assertNotEquals(locator1.hashCode(), locator2.hashCode());
+ }
+
+ @Test
+ void toString_containsTheValue() {
+ SourceDocumentLocator locator = new SourceDocumentLocator(VALID_LOCATOR);
+ assertTrue(locator.toString().contains(VALID_LOCATOR));
+ }
+
+ @Test
+ void sourceDocumentLocatorCanBeUsedAsMapKey() {
+ SourceDocumentLocator locator1 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator2 = new SourceDocumentLocator(VALID_LOCATOR);
+ SourceDocumentLocator locator3 = new SourceDocumentLocator(VALID_LOCATOR_2);
+
+ var map = new java.util.HashMap