fix: Historiendetails mit Token-Spalten laden und Log-Verzeichnis robust anlegen
- SqliteHistoryQueryAdapter.findAttemptsByFingerprint las die Token-/Preis-Spalten, die die SELECT-Liste nicht projizierte -> "no such column: 'input_tokens'" bei jedem Dokument mit Versuchen. Spalten in die SELECT-Liste aufgenommen (konsistent mit dem Schreibpfad in SqliteProcessingAttemptRepositoryAdapter.save). - EarlyLogDirectoryInitializer las die .properties ohne die Backslash-Absicherung des regulaeren Konfig-Lesers; Windows-Pfade wie C:\Users\...\logs wurden von java.util.Properties verstuemmelt, das konfigurierte Log-Verzeichnis nie angelegt und der Fehler blieb unsichtbar. Backslashes vor dem Laden verdoppeln, Pfad normalisieren, Verzeichnis best-effort anlegen; bei Fehlschlag sichtbar auf das Standard-Log-Verzeichnis ausweichen (WARN nach Log4j2-Init). - Regressionstests fuer beide Faelle ergaenzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -215,7 +215,10 @@ public class SqliteHistoryQueryAdapter implements HistoryQueryPort {
|
||||
status, failure_class, failure_message, retryable,
|
||||
ai_provider, model_name, prompt_identifier, processed_page_count, sent_character_count,
|
||||
ai_raw_response, ai_reasoning, resolved_date, date_source, validated_title,
|
||||
final_target_file_name
|
||||
final_target_file_name,
|
||||
input_tokens, output_tokens,
|
||||
cache_creation_input_tokens, cache_read_input_tokens,
|
||||
price_input_per_token_nano_usd, price_output_per_token_nano_usd
|
||||
FROM processing_attempt
|
||||
WHERE fingerprint = ?
|
||||
ORDER BY attempt_number ASC
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package de.gecheckt.pdf.umbenenner.adapter.out.sqlite;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
|
||||
/**
|
||||
* Tests für {@link SqliteHistoryQueryAdapter#findAttemptsByFingerprint}.
|
||||
*
|
||||
* <p>Deckt insbesondere die Regression ab, dass der Historien-Leseadapter die
|
||||
* Token- und Preis-Snapshot-Spalten mappen kann. Die zugehörige SELECT-Liste muss
|
||||
* dieselben Spalten projizieren, die der Mapper liest – andernfalls schlägt jede
|
||||
* Detailabfrage mit {@code no such column: 'input_tokens'} fehl.
|
||||
*/
|
||||
class SqliteHistoryQueryAdapterTest {
|
||||
|
||||
private String jdbcUrl;
|
||||
private SqliteSchemaInitializationAdapter schemaAdapter;
|
||||
private SqliteProcessingAttemptRepositoryAdapter repository;
|
||||
private SqliteHistoryQueryAdapter historyQuery;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
Path dbFile = tempDir.resolve("history-query-test.db");
|
||||
jdbcUrl = "jdbc:sqlite:" + dbFile.toAbsolutePath();
|
||||
schemaAdapter = new SqliteSchemaInitializationAdapter(jdbcUrl);
|
||||
repository = new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
|
||||
historyQuery = new SqliteHistoryQueryAdapter(jdbcUrl);
|
||||
schemaAdapter.initializeSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Versuch mit gesetzten Token- und Preis-Werten muss ohne Exception geladen und
|
||||
* die Werte müssen korrekt zurückgemappt werden.
|
||||
*/
|
||||
@Test
|
||||
void findAttemptsByFingerprint_liestTokenUndPreisSpalten() {
|
||||
DocumentFingerprint fp = fingerprint("aa");
|
||||
insertDocumentRecord(fp);
|
||||
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||
ProcessingAttempt attempt = new ProcessingAttempt(
|
||||
fp, new RunId("run-tokens"), 1, now, now.plusSeconds(1),
|
||||
ProcessingStatus.PROPOSAL_READY,
|
||||
null, null, false,
|
||||
"claude",
|
||||
"claude-haiku-4-5-20251001", "prompt-v1", 3, 4200,
|
||||
"{\"title\":\"x\"}", "reasoning",
|
||||
null, null, "Titel", "2026-07-01 - Titel.pdf",
|
||||
1500L, 800L, 200L, 100L, 250L, 2000L);
|
||||
repository.save(attempt);
|
||||
|
||||
List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);
|
||||
|
||||
assertThat(loaded).hasSize(1);
|
||||
ProcessingAttempt result = loaded.get(0);
|
||||
assertThat(result.inputTokens()).isEqualTo(1500L);
|
||||
assertThat(result.outputTokens()).isEqualTo(800L);
|
||||
assertThat(result.cacheCreationInputTokens()).isEqualTo(200L);
|
||||
assertThat(result.cacheReadInputTokens()).isEqualTo(100L);
|
||||
assertThat(result.priceInputPerTokenNanoUsd()).isEqualTo(250L);
|
||||
assertThat(result.priceOutputPerTokenNanoUsd()).isEqualTo(2000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Versuch ohne Token-/Preis-Werte (typisch für Skip-/Pre-Check-Pfade und
|
||||
* historische Bestände) muss {@code null} für alle Token-Felder zurückliefern,
|
||||
* ohne dass die Abfrage fehlschlägt.
|
||||
*/
|
||||
@Test
|
||||
void findAttemptsByFingerprint_nullTokenWerteBleibenNull() {
|
||||
DocumentFingerprint fp = fingerprint("bb");
|
||||
insertDocumentRecord(fp);
|
||||
|
||||
Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||
ProcessingAttempt attempt = ProcessingAttempt.withoutAiFields(
|
||||
fp, new RunId("run-null"), 1, now, now.plusSeconds(1),
|
||||
ProcessingStatus.FAILED_RETRYABLE, "Err", "msg", true);
|
||||
repository.save(attempt);
|
||||
|
||||
List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);
|
||||
|
||||
assertThat(loaded).hasSize(1);
|
||||
ProcessingAttempt result = loaded.get(0);
|
||||
assertThat(result.inputTokens()).isNull();
|
||||
assertThat(result.outputTokens()).isNull();
|
||||
assertThat(result.cacheCreationInputTokens()).isNull();
|
||||
assertThat(result.cacheReadInputTokens()).isNull();
|
||||
assertThat(result.priceInputPerTokenNanoUsd()).isNull();
|
||||
assertThat(result.priceOutputPerTokenNanoUsd()).isNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mehrere Versuche müssen aufsteigend nach {@code attempt_number} zurückgegeben werden.
|
||||
*/
|
||||
@Test
|
||||
void findAttemptsByFingerprint_sortiertAufsteigend() {
|
||||
DocumentFingerprint fp = fingerprint("cc");
|
||||
insertDocumentRecord(fp);
|
||||
|
||||
Instant base = Instant.now().truncatedTo(ChronoUnit.MICROS);
|
||||
repository.save(ProcessingAttempt.withoutAiFields(
|
||||
fp, new RunId("run-1"), 1, base, base.plusSeconds(1),
|
||||
ProcessingStatus.FAILED_RETRYABLE, "Err", "msg", true));
|
||||
repository.save(new ProcessingAttempt(
|
||||
fp, new RunId("run-2"), 2, base.plusSeconds(10), base.plusSeconds(11),
|
||||
ProcessingStatus.PROPOSAL_READY,
|
||||
null, null, false, "claude",
|
||||
"claude-haiku-4-5-20251001", "prompt-v1", 1, 100,
|
||||
"{}", "r", null, null, "T", "2026-07-01 - T.pdf",
|
||||
10L, 20L, null, null, 250L, 2000L));
|
||||
|
||||
List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);
|
||||
|
||||
assertThat(loaded).hasSize(2);
|
||||
assertThat(loaded.get(0).attemptNumber()).isEqualTo(1);
|
||||
assertThat(loaded.get(1).attemptNumber()).isEqualTo(2);
|
||||
assertThat(loaded.get(1).inputTokens()).isEqualTo(10L);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void insertDocumentRecord(DocumentFingerprint fp) {
|
||||
try (Connection conn = DriverManager.getConnection(jdbcUrl);
|
||||
PreparedStatement ps = conn.prepareStatement("""
|
||||
INSERT INTO document_record
|
||||
(fingerprint, last_known_source_locator, last_known_source_file_name,
|
||||
overall_status, created_at, updated_at)
|
||||
VALUES (?, '/tmp/test.pdf', 'test.pdf', 'READY_FOR_AI',
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
|
||||
strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))""")) {
|
||||
ps.setString(1, fp.sha256Hex());
|
||||
ps.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed to insert test document record", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static DocumentFingerprint fingerprint(String suffix) {
|
||||
return new DocumentFingerprint("0".repeat(64 - suffix.length()) + suffix);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user