M5 AP-003 Unnötige Scope-Änderungen entfernt und Adapter-Tests auf
echten Outbound-Request geschärft
This commit is contained in:
123
WORKFLOW.md
123
WORKFLOW.md
@@ -1,123 +0,0 @@
|
||||
# WORKFLOW – KI-gesteuerte AP-Implementierung
|
||||
|
||||
Dieses Dokument beschreibt verbindlich, wie Claude Code Arbeitspakete eines Meilensteins
|
||||
sequenziell implementiert. Es ist ein technischer Arbeitsrahmen, kein fachliches Dokument.
|
||||
|
||||
---
|
||||
|
||||
## Eingabe
|
||||
|
||||
- Eine Arbeitspakete-MD-Datei des aktiven Meilensteins (z. B. `M6 - Arbeitspakete.md`)
|
||||
- `CLAUDE.md` im Projektroot (Architektur, Konventionen, Nicht-Ziele)
|
||||
- alle verbindlichen Spezifikationsdokumente im Projektroot
|
||||
|
||||
---
|
||||
|
||||
## Grundregeln
|
||||
|
||||
- **Immer zuerst lesen, dann implementieren.** Vor jeder Änderung die relevanten
|
||||
Typen, Ports und Implementierungen im echten Workspace suchen und öffnen.
|
||||
- **Keine Annahmen über Dateipfade.** Typen und Klassen werden per Suche nach
|
||||
Typname gefunden, nicht über vermutete Pfade.
|
||||
- **Kein Git.** Keine git-Befehle, keine Commits, kein Staging, kein Revert.
|
||||
- **Nur Dateioperationen.**
|
||||
|
||||
---
|
||||
|
||||
## Sequenzielle AP-Bearbeitung
|
||||
|
||||
Arbeitspakete werden in der Reihenfolge des Dokuments abgearbeitet.
|
||||
Ein AP ist abgeschlossen, wenn:
|
||||
|
||||
1. der Scope vollständig umgesetzt ist,
|
||||
2. der Build vom Parent-Root fehlerfrei durchläuft,
|
||||
3. das vorgeschriebene Output-Format ausgegeben wurde.
|
||||
|
||||
Erst dann beginnt das nächste AP.
|
||||
|
||||
---
|
||||
|
||||
## Build-Validierung
|
||||
|
||||
Nach jedem AP wird **zwingend** ein Build aus dem Parent-Root ausgeführt:
|
||||
|
||||
```
|
||||
.\mvnw.cmd clean verify -pl pdf-umbenenner-domain,pdf-umbenenner-application,pdf-umbenenner-adapter-out,pdf-umbenenner-adapter-in-cli,pdf-umbenenner-bootstrap --also-make
|
||||
```
|
||||
|
||||
Schlägt der Build fehl:
|
||||
- Fehler analysieren
|
||||
- im selben AP beheben
|
||||
- Build erneut ausführen
|
||||
- erst bei grünem Build weiter zum nächsten AP
|
||||
|
||||
---
|
||||
|
||||
## Pflicht-Output-Format nach jedem AP
|
||||
|
||||
```
|
||||
## AP-XXX Abschluss
|
||||
|
||||
- Scope erfüllt: ja/nein
|
||||
- Geänderte Dateien:
|
||||
- <Dateipfad>
|
||||
- ...
|
||||
- Build-Kommando: .\mvnw.cmd clean verify ...
|
||||
- Build-Status: ERFOLGREICH / FEHLGESCHLAGEN
|
||||
- Offene Punkte: keine / <Beschreibung>
|
||||
- Risiken: keine / <Beschreibung>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope-Kontrolle
|
||||
|
||||
Pro AP gilt verbindlich:
|
||||
|
||||
- **Nur** die im AP beschriebenen Änderungen umsetzen.
|
||||
- **Kein Vorgriff** auf spätere APs oder spätere Meilensteine.
|
||||
- **Kein Refactoring** außerhalb des AP-Scopes.
|
||||
- **Keine kosmetischen Cleanups**, die nicht durch den AP-Scope gedeckt sind.
|
||||
- **Keine Import-Bereinigungen**, die nicht durch konkrete eigene Änderungen erzwungen werden.
|
||||
|
||||
---
|
||||
|
||||
## Naming-Regel (verbindlich für alle APs)
|
||||
|
||||
In geänderten Dateien – Code, Kommentare, JavaDoc – dürfen **keine** Meilenstein-
|
||||
oder Arbeitspaket-Bezeichner erscheinen:
|
||||
|
||||
- Verboten: `M1`, `M2`, `M3`, `M4`, `M5`, `M6`, `M7`, `M8`
|
||||
- Verboten: `AP-001`, `AP-002`, … `AP-00x`
|
||||
|
||||
Stattdessen werden **zeitlose technische Bezeichnungen** verwendet.
|
||||
Wenn bestehende Kommentare solche Bezeichner enthalten und durch den AP-Scope
|
||||
berührt werden, sind sie zu ersetzen.
|
||||
|
||||
---
|
||||
|
||||
## Architekturregeln (Kurzfassung – vollständig in CLAUDE.md)
|
||||
|
||||
- Strenge hexagonale Architektur: Abhängigkeitsrichtung zeigt immer nach innen.
|
||||
- Domain und Application: keine Infrastruktur, kein `Path`/`File`, kein JDBC, kein HTTP.
|
||||
- Adapter-Out: alle Infrastrukturdetails (Dateisystem, SQLite, HTTP, PDFBox).
|
||||
- Adapter dürfen nicht direkt voneinander abhängen.
|
||||
- Ports sind die einzigen Querschnittspunkte.
|
||||
|
||||
---
|
||||
|
||||
## Startkommando für einen vollständigen Meilenstein
|
||||
|
||||
```
|
||||
Lies CLAUDE.md und M6 - Arbeitspakete.md vollständig.
|
||||
Implementiere danach alle Arbeitspakete sequenziell gemäß WORKFLOW.md.
|
||||
Beginne mit AP-001.
|
||||
```
|
||||
|
||||
## Startkommando für ein einzelnes AP
|
||||
|
||||
```
|
||||
Lies CLAUDE.md und M6 - Arbeitspakete.md vollständig.
|
||||
AP-001 bis AP-00X sind bereits abgeschlossen und bilden die Baseline.
|
||||
Implementiere ausschließlich AP-00Y gemäß WORKFLOW.md.
|
||||
```
|
||||
@@ -102,6 +102,9 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
|
||||
private final String apiKey;
|
||||
private final int apiTimeoutSeconds;
|
||||
|
||||
// Test-only field to capture the last built JSON body for assertion
|
||||
private volatile String lastBuiltJsonBody;
|
||||
|
||||
/**
|
||||
* Creates an adapter with configuration from startup configuration.
|
||||
* <p>
|
||||
@@ -236,6 +239,8 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
|
||||
URI endpoint = buildEndpointUri();
|
||||
|
||||
String requestBody = buildJsonRequestBody(request);
|
||||
// Capture for test inspection (test-only field)
|
||||
this.lastBuiltJsonBody = requestBody;
|
||||
|
||||
return HttpRequest.newBuilder(endpoint)
|
||||
.header("Content-Type", CONTENT_TYPE)
|
||||
@@ -304,6 +309,21 @@ public class OpenAiHttpAdapter implements AiInvocationPort {
|
||||
return body.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Package-private accessor for the last constructed JSON body.
|
||||
* <p>
|
||||
* <strong>For testing only:</strong> Allows tests to verify the actual
|
||||
* JSON body sent in HTTP requests without exposing the BodyPublisher internals.
|
||||
* This method is used by unit tests to assert that the correct model, text,
|
||||
* and other fields are present in the outbound request.
|
||||
*
|
||||
* @return the last JSON body string constructed by {@link #buildRequest(AiRequestRepresentation)},
|
||||
* or null if no request has been built yet
|
||||
*/
|
||||
String getLastBuiltJsonBodyForTesting() {
|
||||
return lastBuiltJsonBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the HTTP request and returns the response.
|
||||
* <p>
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpTimeoutException;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -235,19 +236,17 @@ class OpenAiHttpAdapterTest {
|
||||
// Act
|
||||
adapter.invoke(request);
|
||||
|
||||
// Assert - verify the HttpRequest was sent and adapter was initialized with configured timeout
|
||||
// Assert - verify the actual timeout is set on the HttpRequest itself
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).send(requestCaptor.capture(), any());
|
||||
|
||||
// The adapter is initialized with TIMEOUT_SECONDS from the configuration
|
||||
// and uses it in buildRequest() via: .timeout(Duration.ofSeconds(apiTimeoutSeconds))
|
||||
// Verify the configuration was correctly applied to the adapter
|
||||
assertThat(testConfiguration.apiTimeoutSeconds()).isEqualTo(TIMEOUT_SECONDS);
|
||||
|
||||
// Verify the request was sent (proving buildRequest was called with timeout)
|
||||
HttpRequest capturedRequest = requestCaptor.getValue();
|
||||
assertThat(capturedRequest).isNotNull();
|
||||
assertThat(capturedRequest.uri()).isNotNull();
|
||||
// Verify the timeout was actually configured on the request
|
||||
assertThat(capturedRequest.timeout())
|
||||
.as("HttpRequest timeout should be present")
|
||||
.isPresent()
|
||||
.get()
|
||||
.isEqualTo(Duration.ofSeconds(TIMEOUT_SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -277,26 +276,26 @@ class OpenAiHttpAdapterTest {
|
||||
void testConfiguredModelIsUsedInRequestBody() throws Exception {
|
||||
// Arrange
|
||||
AiRequestRepresentation request = createTestRequest("Test prompt", "Test document");
|
||||
|
||||
// Act - directly build the JSON body to verify model is present
|
||||
String jsonBody = adapter.buildJsonRequestBody(request);
|
||||
|
||||
// Assert - verify the actual JSON body contains the configured model
|
||||
JSONObject bodyJson = new JSONObject(jsonBody);
|
||||
assertThat(bodyJson.getString("model")).isEqualTo(API_MODEL);
|
||||
|
||||
// Also verify in actual request
|
||||
HttpResponse<String> httpResponse = mockHttpResponse(200, "{}");
|
||||
when(httpClient.send(any(HttpRequest.class), any())).thenReturn((HttpResponse) httpResponse);
|
||||
|
||||
// Act - invoke to trigger actual request building
|
||||
adapter.invoke(request);
|
||||
|
||||
// Assert - verify model is in the actual request body that was sent
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).send(requestCaptor.capture(), any());
|
||||
|
||||
// HttpRequest body is in a BodyPublisher, but we've verified the model is in the JSON
|
||||
// and that JSON is what gets sent via BodyPublishers.ofString(requestBody)
|
||||
assertThat(testConfiguration.apiModel()).isEqualTo(API_MODEL);
|
||||
// Get the actual body that was sent in the request via test accessor
|
||||
String sentBody = adapter.getLastBuiltJsonBodyForTesting();
|
||||
assertThat(sentBody)
|
||||
.as("The actual HTTP request body should contain the configured model")
|
||||
.isNotNull();
|
||||
|
||||
JSONObject bodyJson = new JSONObject(sentBody);
|
||||
assertThat(bodyJson.getString("model"))
|
||||
.as("Model in actual request body must match configuration")
|
||||
.isEqualTo(API_MODEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -340,32 +339,38 @@ class OpenAiHttpAdapterTest {
|
||||
sentCharacterCount
|
||||
);
|
||||
|
||||
// Act - directly build the JSON body to verify full text is present
|
||||
String jsonBody = adapter.buildJsonRequestBody(request);
|
||||
HttpResponse<String> httpResponse = mockHttpResponse(200, "{}");
|
||||
when(httpClient.send(any(HttpRequest.class), any())).thenReturn((HttpResponse) httpResponse);
|
||||
|
||||
// Assert - verify the actual JSON body contains the FULL document text, not truncated
|
||||
JSONObject bodyJson = new JSONObject(jsonBody);
|
||||
// Act - invoke to trigger actual request building
|
||||
adapter.invoke(request);
|
||||
|
||||
// Assert - verify the full document text is in the actual request body sent (not truncated)
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).send(requestCaptor.capture(), any());
|
||||
|
||||
// Get the actual body that was sent in the request via test accessor
|
||||
String sentBody = adapter.getLastBuiltJsonBodyForTesting();
|
||||
assertThat(sentBody)
|
||||
.as("The actual HTTP request body should contain the full document text")
|
||||
.isNotNull();
|
||||
|
||||
JSONObject bodyJson = new JSONObject(sentBody);
|
||||
JSONArray messages = bodyJson.getJSONArray("messages");
|
||||
JSONObject userMessage = messages.getJSONObject(1); // User message is second
|
||||
String contentInBody = userMessage.getString("content");
|
||||
|
||||
// Prove the full text is sent, not truncated to sentCharacterCount
|
||||
assertThat(contentInBody).isEqualTo(fullDocumentText);
|
||||
assertThat(contentInBody.length()).isEqualTo(fullDocumentText.length());
|
||||
assertThat(contentInBody).isNotEqualTo(fullDocumentText.substring(0, sentCharacterCount));
|
||||
|
||||
// Also verify in actual request
|
||||
HttpResponse<String> httpResponse = mockHttpResponse(200, "{}");
|
||||
when(httpClient.send(any(HttpRequest.class), any())).thenReturn((HttpResponse) httpResponse);
|
||||
|
||||
adapter.invoke(request);
|
||||
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).send(requestCaptor.capture(), any());
|
||||
|
||||
// Confirm the full text is in the document, not truncated
|
||||
assertThat(request.documentText()).isEqualTo(fullDocumentText);
|
||||
assertThat(request.documentText().length()).isGreaterThan(sentCharacterCount);
|
||||
// Prove the full text is sent in the actual request, not truncated by sentCharacterCount
|
||||
assertThat(contentInBody)
|
||||
.as("Document text in actual request body must be the full text")
|
||||
.isEqualTo(fullDocumentText);
|
||||
assertThat(contentInBody)
|
||||
.as("Sent text must not be truncated to sentCharacterCount")
|
||||
.isNotEqualTo(fullDocumentText.substring(0, sentCharacterCount));
|
||||
assertThat(contentInBody.length())
|
||||
.as("Text length must match full document, not truncated")
|
||||
.isEqualTo(fullDocumentText.length())
|
||||
.isGreaterThan(sentCharacterCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user