Dokumentiert die 24h-Dauerlauf-Abweichung (komprimierter Ersatztest statt woertlichem 24h-Lauf, User-autorisiert), alle drei Akzeptanzkriterien mit Zielwert/Messwert/Bewertung, den gefundenen und behobenen Goroutine-Leck in mailimport.Dialer.connect, sowie den Regressionsnachweis ueber die vollstaendige DMS-Testsuite und die QA-09-relevanten Root-Modul-Pakete. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
18 KiB
QA-09 – Prüfprotokoll: Schnittstellen-Vertragstests
WICHTIGE, AUSDRÜCKLICH AUTORISIERTE ABWEICHUNG VOM TICKET
Das Ticket QA-09 verlangt unter "Prüfungen vor der Abnahme" wörtlich:
- Dauerlauf-Import über 24 Stunden ohne Verlust
Ein echter 24-Stunden-Dauerlauf ist innerhalb einer einzelnen Claude-Code-Sitzung nicht durchführbar (keine 24-Stunden-Sitzungsbindung). Diese Abweichung wurde vom Nutzer für diese Sitzung ausdrücklich autorisiert (gleiches Vorgehen wie bei anderen Tickets dieses Projekts mit zeitbasierten Dauerlauf-Anforderungen).
Ersatzprüfung in diesem Protokoll: ein komprimierter Dauerlauf mit vielen Import-Zyklen hintereinander statt vieler Stunden Wartezeit — die Garantie "kein Dokumentverlust über viele Zyklen, keine Verschlechterung bei Wiederholung" ist unabhängig von der Zeitspanne, über die sich diese Zyklen in der Praxis verteilen würden:
- E-Mail-Import (INT-04): 30 Postfach-Abrufzyklen, je 3 Nachrichten mit Anhang = 90 Nachrichten / 180 erwartete Dokumente.
- Hot-Folder-Import (INT-06): 25 Stapel-Einlieferungszyklen, je 3 Trennseiten-Teildokumente = 75 erwartete Dokumente, plus eine erneute Einlieferung des letzten Stapels zum Dublettennachweis.
- Watch-Folder-Import (INT-08): 40 Scan-Zyklen, je 4 Dateien = 160 erwartete Dokumente.
Der echte 24-Stunden-Produktionsnachweis (z. B. als überwachter Dauerbetrieb auf einem Testsystem) bleibt ein separater, späterer operativer Schritt und ist nicht Bestandteil dieser Abnahme.
Ergebnis der Ersatzprüfung — nicht folgenlos: Der komprimierte Dauerlauf hat
ein reales Ressourcenleck in dms/internal/mailimport/imap.go aufgedeckt (siehe
Abschnitt "Fund während der Prüfung" unten) und behoben. Genau das ist der Sinn
dieser Art Prüfung — siehe Einzelheiten weiter unten.
Ausgangslage und Rahmenbedingungen dieser Prüfung
- Arbeitsumgebung: Host 192.168.1.204, frisches Wegwerf-Verzeichnis
/root/qa-09-work-1792099372/repo(Clone vonhttps://gitea.perlbach24.de/scripte/nexarch.git). - Branch-Basis:
mainenthält alle in der Ausgangslage genannten Voraussetzungen außerINT-12(AI-Anbindung, DMS-Variante,dms/internal/moduletypeclient+ai/internal/moduletypeapi) — dieser Code liegt bislang nur auf dem unabhängigen, noch nicht gemergten Branchfeature/int-12-ai-anbindung-objekttyp-registrierung-kopplung-ai-gov-01(ein einzelner Commit3589644direkt auf dem aktuellenmain-Standdac7fff, sonst identisch zumain). Da QA-09 INT-12 ausdrücklich als Prüfgegenstand nennt, wurde der QA-09-Branch von diesem INT-12-Branch-Tip abgezweigt statt vonmaindirekt — inhaltlich identisch zu "mainplus genau der eine INT-12-Commit", keine sonstige Abweichung. - Branch:
feature/qa-09-schnittstellen-vertragstests - Testdatenbank: eigene Wegwerf-Rolle/DB auf Host 204 (
qa09_test/dms_qa09_test, Passwort nur in/etc/nexarch-dev/qa09-test.env, root-only). Jeder Testlauf legt zusätzlich pro Paket/Test ein eigenes Postgres-Schema an (Musterdms/internal/testenv,dms/internal/testdb) — keine Kollision mit anderen Sitzungen oder derarchivdms-Produktivdatenbank auf demselben Host. - CMIS (INT-09): hat mit
dms/internal/cmis/parity_test.goundarch_test.gobereits eine vollständige, laufende Vertragsprüfung (CMIS-1.1- Browser-Binding-Konformität plus Architekturregel "nur über die REST-Kette"). Diese wurde nicht dupliziert (Ticket-Vorgabe: "Baue auf ihnen auf, statt Funktionalität zu duplizieren").
Akzeptanzkriterium 1: Vertragstests laufen in der Pipeline
Umsetzung: dms/internal/contracttest (neues Paket, Muster identisch zu
internal/contracttest auf dem Core-Board/QA-07 und archive/internal/ contracttest) — vier Verträge, je mit Hauptprüfung + DetectsBreakingChange-
Gegenprobe:
| Vertrag | Datei | Prüft |
|---|---|---|
| INT-04 (E-Mail-Regeln) | mailrule_test.go |
JSON-Schema der echten mailimportapi-Antwort gegen die in INT-04 dokumentierten Pflichtfelder (u. a. attachment_mime für den Anhangtyp-Zugriff) |
| INT-11 (Archive-Anbindung) | retentionclient_test.go |
Tatsächlich gesendete Registrierungs-Nutzlast von retentionclient.Client.Register gegen einen echten HTTP-Testempfänger |
| INT-12 (AI-Anbindung) | moduletypeclient_test.go |
Tatsächlich gesendete Nutzlast von moduletypeclient.Client.Register, inkl. KI-relevanter Feld-Liste (fields[].name/kind) |
| INT-13 (Workflow-Anbindung) | workflowevent_test.go |
Registrierungs- und Ereignis-Zustellungs-Nutzlast von workflowevent.Register/workflowevent.Handler |
Zusätzlich (Core-Board, da internal/webhook nur im Core-Modul liegt):
internal/contracttest/webhook_retry_test.go für API-07 (siehe
Akzeptanzkriterium 3).
Pipeline-Nachweis (kein neuer Workflow nötig): Diese Pakete sind normale
Go-Testpakete ohne Build-Tags. Die bestehende CI-Pipeline deckt sie bereits ab,
ohne Änderung an .gitea/workflows/:
.gitea/workflows/ci.yml, Jobdms-build-test:go build ./... && go vet ./... && go test ./... -p 1 -count=1im Arbeitsverzeichnisdms— läuft bei jedem Push/PR und schließtdms/internal/contracttest,dms/internal/mailimport,dms/internal/watchfolderautomatisch ein..gitea/workflows/ci.yml, Jobgo-core:go test ./...im Root-Modul — schließtinternal/contracttest(inkl.webhook_retry_test.go) automatisch ein..gitea/workflows/contract-tests.ymltriggert zusätzlich explizit aufpush-Änderungen unterinternal/contracttest/**sowie auf jeden Pull Request (pull_request: {}ohne Pfadfilter).
Zielwert: Ein entferntes/umbenanntes Pflichtfeld in einer der vier DMS-Schnittstellen oder bei der Webhook-Zustellung lässt den jeweiligen Vertragstest fehlschlagen → CI-Job rot.
Messwert: Alle 9 neuen Vertragstests (4 Hauptprüfungen + 4
DetectsBreakingChange-Gegenproben + 1 Webhook-Signatur-Altbestand) laufen
grün; jede DetectsBreakingChange-Gegenprobe wurde selbst gegen eine absichtlich
verstümmelte Fixture geprüft und schlägt dort nachweislich fehl (siehe
Testausgabe unten) — das beweist, dass die Prüfung bei einer echten Abweichung
tatsächlich auslöst, nicht nur im Erfolgsfall grün ist.
Bewertung: erfüllt.
$ go test ./internal/contracttest/... -v -count=1 (im Verzeichnis dms/)
=== RUN TestContract_INT04_MailRuleSchema
--- PASS: TestContract_INT04_MailRuleSchema (0.48s)
=== RUN TestContract_INT04_DetectsBreakingChange_RemovedAttachmentMimeField
--- PASS: TestContract_INT04_DetectsBreakingChange_RemovedAttachmentMimeField (0.00s)
=== RUN TestContract_INT12_ModuleTypeRegisterPayload
--- PASS: TestContract_INT12_ModuleTypeRegisterPayload (0.00s)
=== RUN TestContract_INT12_DetectsBreakingChange_RenamedFieldKind
--- PASS: TestContract_INT12_DetectsBreakingChange_RenamedFieldKind (0.00s)
=== RUN TestContract_INT11_RetentionRegisterPayload
--- PASS: TestContract_INT11_RetentionRegisterPayload (0.00s)
=== RUN TestContract_INT11_DetectsBreakingChange_RemovedObjectTypeField
--- PASS: TestContract_INT11_DetectsBreakingChange_RemovedObjectTypeField (0.00s)
=== RUN TestContract_INT13_WorkflowRegisterPayload
--- PASS: TestContract_INT13_WorkflowRegisterPayload (0.00s)
=== RUN TestContract_INT13_DetectsBreakingChange_RemovedTriggersField
--- PASS: TestContract_INT13_DetectsBreakingChange_RemovedTriggersField (0.00s)
=== RUN TestContract_INT13_WorkflowEventPublishPayload
--- PASS: TestContract_INT13_WorkflowEventPublishPayload (0.00s)
PASS
ok gitea.perlbach24.de/scripte/nexarch/dms/internal/contracttest 0.488s
Akzeptanzkriterium 2 / Prüfung 2: Import über E-Mail und Hot-Folder verliert keine Dokumente (komprimierter Dauerlauf)
Umsetzung: zwei neue Testdateien, jeweils im selben Paket wie der zu prüfende Code (keine Duplizierung bestehender Testinfrastruktur wie dem Fake-IMAP-Server oder der Guard-/Stapel-Testumgebung, die beide nur paketintern nutzbar sind):
dms/internal/mailimport/qa09_endurance_test.go—TestQA09_MailImport_ManyPollCyclesNoDocumentLossNoLeakdms/internal/watchfolder/qa09_endurance_test.go—TestQA09_WatchFolder_ManyCyclesNoDocumentLossNoLeak(INT-08) undTestQA09_HotFolder_ManyStacksNoDocumentLossNoDuplicates(INT-06)
Zielwert: nach N Import-Zyklen entspricht die tatsächliche Dokumentanzahl
in der Datenbank exakt der Anzahl eingelieferter Dateien/Nachrichten-Teile
(keine Lücke, keine Dublette), der Quellordner ist restlos leer (abgesehen von
den vorgesehenen error/duplicate-Unterordnern), und die Goroutine-Anzahl
vor/nach dem Dauerlauf bleibt im selben Bereich (Toleranz: höchstens +10, um
normale Laufzeit-Schwankungen von Go/Test-Runtime nicht fälschlich als Leck zu
werten).
Messwert (nach Behebung des unten beschriebenen Fundes):
$ go test ./internal/mailimport/... -run TestQA09 -v -count=1
=== RUN TestQA09_MailImport_ManyPollCyclesNoDocumentLossNoLeak
--- PASS: TestQA09_MailImport_ManyPollCyclesNoDocumentLossNoLeak (11.11s)
PASS
ok gitea.perlbach24.de/scripte/nexarch/dms/internal/mailimport 11.113s
$ go test ./internal/watchfolder/... -run TestQA09 -v -count=1
=== RUN TestQA09_WatchFolder_ManyCyclesNoDocumentLossNoLeak
--- PASS: TestQA09_WatchFolder_ManyCyclesNoDocumentLossNoLeak (5.56s)
=== RUN TestQA09_HotFolder_ManyStacksNoDocumentLossNoDuplicates
--- PASS: TestQA09_HotFolder_ManyStacksNoDocumentLossNoDuplicates (2.69s)
PASS
ok gitea.perlbach24.de/scripte/nexarch/dms/internal/watchfolder 8.287s
- E-Mail: 90 Nachrichten über 30 Abrufzyklen → 180 Dokumente erwartet, 180
tatsächlich (90
.eml-Kontextdateien + 90 Anhänge), 0 Zurückweisungen. - Watch-Folder: 160 Dateien über 40 Zyklen → 160 Dokumente, Quellordner leer, keine Ablehnung.
- Hot-Folder: 25 Stapel à 3 Teile über Trennseiten → 75 Dokumente, 0 Einträge im Fehler-/Duplikat-Ordner; eine absichtlich wiederholte Einlieferung des letzten Stapels erzeugt nachweislich keine zusätzliche Dublette (Prüfung 3 aus INT-06, hier zusätzlich über viele vorausgegangene Zyklen hinweg bestätigt statt nur isoliert).
Bewertung: erfüllt.
Fund während der Prüfung: Goroutine-Leck in dms/internal/mailimport/imap.go
Der komprimierte Dauerlauf-Test für den E-Mail-Import schlug beim ersten Lauf fehl:
qa09_endurance_test.go:82: goroutine-anzahl vor/nach 30 poll-zyklen: 5 -> 35
(hinweis auf ressourcenleck, z.b. nicht geschlossene imap-sitzungen)
Ursache: Dialer.connect startete pro IMAP-Verbindung eine Goroutine, die
auf <-ctx.Done() wartet, um die Verbindung bei Abbruch zu beenden:
s := &session{c: c}
go func() {
<-ctx.Done()
_ = c.Terminate()
}()
ctx ist hier der vom Aufrufer übergebene Kontext (Importer.Poll wird in
Produktion und in den Tests regelmäßig mit context.Background() aufgerufen,
der nie "Done" wird). Die Goroutine wartete damit für immer auf einen Kanal,
der nie etwas liefert — unabhängig davon, ob session.close() die Verbindung
längst regulär beendet hatte. Jeder Postfach-Abruf hinterließ so dauerhaft eine
Goroutine; bei einem echten 24-Stunden-Dauerbetrieb mit mehreren Postfächern im
Minutentakt hätte sich das über die Zeit messbar aufgebaut (genau die
"Verschlechterung bei Wiederholung", die Prüfung 2 ausschließen soll).
Fix (dms/internal/mailimport/imap.go): die Beobachtungs-Goroutine
beobachtet jetzt einen von ctx abgeleiteten, an die Sitzung gebundenen
Kontext, der in session.close() immer abgebrochen wird — das externe
Abbruchverhalten (Terminate bei Kontext-Stornierung während eines laufenden
IMAP-Aufrufs) bleibt dabei unverändert erhalten:
sessCtx, cancel := context.WithCancel(ctx)
s := &session{c: c, cancel: cancel}
go func() {
<-sessCtx.Done()
_ = c.Terminate()
}()
...
func (s *session) close() {
_ = s.c.Logout()
_ = s.c.Terminate()
s.cancel()
}
Nach dem Fix: Goroutine-Anzahl bleibt über 30 Zyklen stabil (siehe
Testausgabe oben, Test läuft grün). Zusätzlich per runtime.Stack(..., true)
in einer Wegwerf-Probe (nicht Teil des Commits) verifiziert, dass vor dem Fix
exakt so viele dauerhaft blockierte chan receive (nil chan)-Goroutinen in
Dialer.connect.func1 vorlagen wie Poll-Zyklen durchlaufen wurden — nach dem
Fix verschwanden sie vollständig.
Die vollständige bestehende mailimport-Testsuite (20 Testfunktionen, u. a.
TLS/StartTLS, abgebrochene Verbindungen, parallele Postfächer) läuft nach dem
Fix weiterhin grün (siehe Abschnitt "Gesamt-Testlauf" unten).
Akzeptanzkriterium 3 / Prüfung 3: Wiederholungsverhalten bei Ausfall belegt
Umsetzung: internal/contracttest/webhook_retry_test.go (Core-Modul, da
internal/webhook — API-07 — nur dort liegt; DMS ist ein eigenes Go-Modul und
kann Core-interne Pakete nicht importieren, dasselbe Argument wie bei
dms/internal/retentionclient/moduletypeclient für Archive/AI).
Die bestehende Core-Testsuite (internal/webhook/webhook_test.go) belegt
bereits den dauerhaften Fehlschlag nach Erreichen von MaxAttempts
(TestProcessDue_RetriesWithBackoffThenMarksFailed). QA-09 verlangt zusätzlich
den für ein konsumierendes Modul wie DMS praxisrelevanten Fall: ein Ziel, das
zeitweise nicht erreichbar ist und danach wieder antwortet, wird trotzdem
noch zugestellt — das ist die neue Prüfung hier.
Aufbau: ein httptest.Server antwortet auf die ersten zwei Zustellversuche
mit 503 Service Unavailable, ab dem dritten Versuch mit 200 OK. Der echte
webhook.Dispatcher (kleine Backoff-Zeit für den Test, reale Retry-/Backoff-
Logik unverändert) ruft ProcessDue wiederholt auf, bis entweder zugestellt
oder endgültig fehlgeschlagen.
Zielwert: der Zustellungsdatensatz erreicht status = delivered, und es
wurden tatsächlich mehr als 2 HTTP-Aufrufe gegen das Ziel unternommen (echter
Retry, nicht Zufall/Direkttreffer).
Messwert:
$ go test ./internal/contracttest/... -run TestContract_API07_RetryRecoversAfterTransientFailures -v -count=1 (im Root-Modul)
=== RUN TestContract_API07_RetryRecoversAfterTransientFailures
--- PASS: TestContract_API07_RetryRecoversAfterTransientFailures (0.27s)
PASS
ok gitea.perlbach24.de/scripte/nexarch/internal/contracttest 0.351s
Zustellung erreichte status = delivered nach 3 tatsächlichen HTTP-Aufrufen
(2 Fehlschläge mit exponentiellem Backoff, danach Erfolg) — die Retry-Logik aus
API-07 greift nachweislich.
Bewertung: erfüllt.
Prüfung 1 (separat benannt im Ticket): Abweichung zwischen Beschreibung und Verhalten bricht die Pipeline
Siehe Akzeptanzkriterium 1 oben — die vier DetectsBreakingChange-Gegenproben
sind der Nachweis, dass eine simulierte Schema-Abweichung den jeweiligen Test
tatsächlich rot werden lässt (nicht nur behauptet wird). Da diese Gegenproben
dauerhaft im Testbaum bleiben (Muster identisch zu Core-QA-07/Archive), ist
die Prüfung bei jedem künftigen CI-Lauf automatisch reproduzierbar, nicht nur
einmalig in dieser Sitzung durchgeführt.
Bewertung: erfüllt.
Gesamt-Testlauf (Regressionsnachweis)
Zusätzlich zu den oben gezeigten gezielten Läufen wurde die gesamte
bestehende DMS-Testsuite nach allen Änderungen erneut ausgeführt
(go build ./... && go vet ./... && go test ./... -p 1 -count=1, identisch zum
CI-Job dms-build-test), um Regressionen in angrenzenden Paketen durch den
Fix in imap.go auszuschließen:
$ go build ./... && go vet ./... && go test ./... -p 1 -count=1
(Build: keine Ausgabe = Erfolg; go vet: keine Ausgabe = keine Befunde)
96 Pakete mit Tests: alle "ok", 0 "FAIL".
18 Pakete ohne Testdateien ([no test files], z.B. reine cmd/-Einstiegspunkte).
Vollständige Paketliste inkl. Laufzeiten liegt im Sitzungs-Log vor; u. a.
bestätigt grün: dms/internal/contracttest (neu), dms/internal/mailimport
(20 Testfunktionen + 1 neue), dms/internal/watchfolder (18 Testfunktionen +
2 neue), dms/internal/mailimportapi, dms/internal/retentionclient,
dms/internal/workflowevent, dms/internal/moduletypeclient,
dms/internal/cmis. go vet ./... über das gesamte Modul: keine Befunde.
Root-Modul (Core): internal/contracttest (alle 9 Tests inkl. der neuen
API-07-Wiederholungsprüfung) und internal/webhook selbst laufen beide
isoliert grün. Ein erster Versuch, zusätzlich die gesamte
Root-Modul-Testsuite (go test ./...) als Regressionsnachweis mitlaufen zu
lassen, zeigte Dutzende Fehlschläge in völlig unbeteiligten Paketen
(internal/tenant, internal/user, internal/auth, internal/rbac,
internal/migrate*, …) — Ursache war ausschließlich eine fehlende
Berechtigung der für diese Sitzung frisch angelegten Testrolle
(GRANT CREATE ON DATABASE postgres TO qa09_test fehlte; Core-Tickets wie
das Tenant-Onboarding legen für ihre Tests echte, eigene physische
Datenbanken an, nicht nur Schemata). Nach dem Grant liefen
internal/tenant/internal/user isoliert erneut grün. Dieser Teil war
reine Testumgebungs-Konfiguration dieser Wegwerf-Sitzung, keine
Codeänderung durch QA-09.
Der anschließende zweite Gesamtlauf der Root-Modul-Suite mit korrigierter
Berechtigung lief zum Zeitpunkt der Protokollerstellung noch; bis dahin war
außer der bereits unten beschriebenen, nachweislich vorbestehenden
TestAllHTTPServersUseHTTPHarden-Rotmarkierung keine weitere Abweichung
aufgetreten. Dieser Gesamtlauf dient ausschließlich der zusätzlichen
Absicherung gegen Fernwirkungen auf das Root-Modul insgesamt — die für
QA-09 selbst relevanten Pakete (internal/contracttest,
internal/webhook) sind bereits isoliert und vollständig grün bestätigt
(siehe Akzeptanzkriterium 3 oben), das ist die für die Abnahme
entscheidende Prüfung.
Zur Bestätigung, dass TestAllHTTPServersUseHTTPHarden nicht durch diesen
Branch verursacht ist, wurde sie zusätzlich gegen einen unberührten
frischen main-Checkout ausgeführt und schlägt dort identisch fehl (zwei
ai/cmd/*-a11y-fixture-Programme rufen http.ListenAndServe direkt auf,
unabhängig von QA-09) — eine vorbestehende, nicht ticketbezogene
Fehlermarkierung.
Nicht Bestandteil dieser Kachel
Wie im Ticket vorgegeben: keine vorauseilende Umsetzung. Insbesondere nicht
angefasst: CMIS-Feinheiten über die bestehende Parität hinaus, Webhook-
Registrierung/-Verwaltung selbst (API-07 ist Core-Bestandsfunktion), eigene
Retry-Logik im DMS für Workflow-Events (nutzt bereits die bestehende
jobqueue-Infrastruktur aus FDN-04, unverändert).