Merge PR #349: QA-07: Schnittstellen-Vertragstests (AI)
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
AI-Schnittstellen-Vertragstests / contract-tests (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s

This commit is contained in:
patrick committed 2026-10-19 02:24:28 +02:00
commit 223d07ca96
4 files changed
+219

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
name: AI-Schnittstellen-Vertragstests
on:
push:
paths:
- "ai/internal/contracttest/**"
- "ai/internal/moduletype/**"
pull_request:
paths:
- "ai/internal/contracttest/**"
- "ai/internal/moduletype/**"
jobs:
contract-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Vertragstests ausfuehren
working-directory: ai
run: go test ./internal/contracttest/... -v
+40
View File
@@ -0,0 +1,40 @@
# QA-07 – Prüfprotokoll
Datum: 2026-10-17. Host: Entwicklungshost 192.168.1.204, Wegwerf-Cluster (Port 5433, eigene Rolle/DB `aiqa07test`/`aiqa07testdb`). Frischer Klon von `main`.
## Umsetzung
Neues Paket `ai/internal/contracttest`: Vertragstests für die Modul-Adapter-Schnittstelle GOV-01 (`ai/internal/moduletype`), über die DMS und Mail KI-fähige Objekttypen registrieren. Gleiches Muster wie Archive QA-06 (`archive/internal/contracttest`): `RequireJSONFields` dekodiert eine echte JSON-Antwort und meldet fehlende Pflichtschlüssel.
Zwei Verträge geprüft, jeweils gegen die TATSÄCHLICHE Antwort der echten Komponente, nicht gegen eine nachgebaute Attrappe:
- **Vertrag 1 – registrierter Objekttyp** (`moduletype.ObjectType`): echte `Registry.Register`/`Get`-Rückgabe (gegen Wegwerf-Postgres) JSON-kodiert, Pflichtfelder `TenantSlug`, `ModuleName`, `ObjectType`, `Fields` geprüft. Läuft nur mit `TEST_TENANT_DSN` (sonst Skip, wie bei Archive QA-06).
- **Vertrag 2 – Feld-Schema** (`moduletype.Field`): echte JSON-Kodierung eines `Field`-Werts geprüft auf `name`, `kind`. Läuft immer, ohne Datenbank.
Zu jedem Vertrag ein Test, der einen simulierten Breaking Change (entferntes bzw. umbenanntes Pflichtfeld) gegen `RequireJSONFields` prüft und dessen Fehlschlagen erwartet (AK2).
CI-Integration: `.gitea/workflows/ai-contract-tests.yml`, ausgelöst bei Änderungen unter `ai/internal/contracttest/**` oder `ai/internal/moduletype/**`, führt `go test ./internal/contracttest/... -v` automatisiert aus (AK3).
## Akzeptanzkriterien
| # | Kriterium | Ergebnis |
|---|---|---|
| 1 | Vertragstests decken die Modul-Adapter-Schnittstelle GOV-01 ab | **Bestanden.** `TestContract_GOV01_RegisteredObjectTypeSchema` (Objekttyp-Rückgabe) und `TestContract_GOV01_FieldSchema` (Feld-Schema) prüfen beide die echte `moduletype`-Komponente. |
| 2 | Ein absichtlich brechender Schema-Wechsel lässt die Vertragstests fehlschlagen | **Bestanden.** Siehe Prüfung 2 unten: Umbenennung von `json:"name"` auf `json:"field_name"` in `moduletype.Field` live im Code herbeigeführt, `TestContract_GOV01_FieldSchema` schlägt fehl mit `field-schema fehlen pflichtfelder: [name]`, danach zurückgesetzt. |
| 3 | Vertragstests laufen automatisiert in der CI, nicht nur manuell | **Bestanden.** `.gitea/workflows/ai-contract-tests.yml` führt die Tests bei Push/PR auf die betroffenen Pfade automatisch aus. |
## Prüfungen vor der Abnahme
| # | Prüfung | Zielwert | Messwert | Ergebnis |
|---|---|---|---|---|
| 1 | Testlauf gegen aktuellen Stand | 0 Fehlschläge | `go test ./internal/contracttest/... -v`: 4/4 Tests PASS | **Bestanden** |
| 2 | Simulierte Breaking Change wird erkannt | Testlauf nach Rename schlägt fehl, nach Rückbau wieder grün | Rename `json:"name"` → `json:"field_name"` in `moduletype.go`: `TestContract_GOV01_FieldSchema` FAIL (`fehlen pflichtfelder: [name]`); nach `git checkout -- internal/moduletype/moduletype.go`: 4/4 Tests wieder PASS | **Bestanden** |
| 3 | CI-Integration nachgewiesen | Workflow-Datei mit passenden `paths`-Filtern vorhanden | `.gitea/workflows/ai-contract-tests.yml` vorhanden, Trigger auf `ai/internal/contracttest/**` und `ai/internal/moduletype/**`, Schritt `go test ./internal/contracttest/... -v` | **Bestanden** |
## Testergebnis
4/4 neue Tests grün (`go test ./internal/contracttest/... -v`), `go build ./...` und `go vet ./...` im AI-Modul sauber. Volle Modul-Regression (`go test ./...`) ohne Fehler, keine bestehenden Tests betroffen.
## Hinweis zur Datenbankabdeckung
Wie bei Archive QA-06 läuft Vertrag 1 (Objekttyp-Rückgabe) nur mit gesetztem `TEST_TENANT_DSN` gegen eine echte Postgres-Instanz; in der CI ohne diese Variable wird dieser Test übersprungen (`t.Skip`), nicht als Fehler gewertet. Vertrag 2 (Feld-Schema) sowie beide Breaking-Change-Tests laufen immer, auch ohne Datenbank — das ist der in der CI tatsächlich scharfe Teil der Vertragstests. Kein offener Befund, aber bewusste Einschränkung, identisch zum etablierten Muster.
+30
View File
@@ -0,0 +1,30 @@
// Package contracttest implementiert AI QA-07: Vertragstests für die
// Modul-Adapter-Schnittstelle GOV-01 (ai/internal/moduletype), über die DMS
// und Mail KI-fähige Objekttypen registrieren. Jeder Test prüft die
// TATSÄCHLICHE Antwort der echten moduletype.Registry gegen ihr
// dokumentiertes Schema — eine entfernte oder umbenannte Pflichteigenschaft
// lässt den jeweiligen Test fehlschlagen, BEVOR sie DMS und Mail gleichzeitig
// bricht (Akzeptanzkriterium 2). Gleiches Muster wie Archive QA-06
// (archive/internal/contracttest).
package contracttest
import (
"encoding/json"
"fmt"
)
// RequireJSONFields dekodiert data als JSON-Objekt und prüft, dass ALLE
// angegebenen Top-Level-Schlüssel vorhanden sind. Liefert die fehlenden
// Schlüssel zurück — leer bedeutet: Vertrag eingehalten.
func RequireJSONFields(data []byte, required []string) (missing []string, err error) {
var decoded map[string]any
if err := json.Unmarshal(data, &decoded); err != nil {
return nil, fmt.Errorf("contracttest: antwort ist kein json-objekt: %w", err)
}
for _, field := range required {
if _, ok := decoded[field]; !ok {
missing = append(missing, field)
}
}
return missing, nil
}
@@ -0,0 +1,126 @@
package contracttest
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
)
// --- Vertrag 1: GOV-01 registrierter Objekttyp (moduletype.ObjectType) ---
// Akzeptanzkriterium 1 + Prüfung 1.
type fakeVerifier struct{}
func (fakeVerifier) VerifyTenant(_ context.Context, _ string) (bool, error) { return true, nil }
func setupRegistry(t *testing.T) *moduletype.Registry {
t.Helper()
dsn := os.Getenv("TEST_TENANT_DSN")
if dsn == "" {
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
}
pool := testdb.Pool(t, dsn)
migrationsDir, err := filepath.Abs("../../migrations")
if err != nil {
t.Fatalf("migrationsverzeichnis: %v", err)
}
migrations, err := migrate.Load(migrationsDir)
if err != nil {
t.Fatalf("migrationen laden: %v", err)
}
if _, err := migrate.Up(context.Background(), pool, migrations); err != nil {
t.Fatalf("migrationen anwenden: %v", err)
}
return &moduletype.Registry{Pool: pool, Verifier: fakeVerifier{}}
}
// TestContract_GOV01_RegisteredObjectTypeSchema prüft das TATSÄCHLICHE,
// von der echten Registry gelieferte ObjectType gegen das dokumentierte
// Schema (Pflichtfelder, auf die DMS/Mail sich als registrierende Module
// verlassen dürfen).
func TestContract_GOV01_RegisteredObjectTypeSchema(t *testing.T) {
r := setupRegistry(t)
ctx := context.Background()
if err := r.Register(ctx, "mandant-vertragstest", "contracttest-dms", "document", []moduletype.Field{
{Name: "title", Kind: "text"},
}); err != nil {
t.Fatalf("register: %v", err)
}
got, err := r.Get(ctx, "mandant-vertragstest", "contracttest-dms", "document")
if err != nil {
t.Fatalf("get: %v", err)
}
encoded, err := json.Marshal(got)
if err != nil {
t.Fatalf("objecttype kodieren: %v", err)
}
required := []string{"TenantSlug", "ModuleName", "ObjectType", "Fields"}
missing, err := RequireJSONFields(encoded, required)
if err != nil {
t.Fatalf("antwort nicht lesbar: %v", err)
}
if len(missing) != 0 {
t.Fatalf("objekttyp-antwort fehlen pflichtfelder: %v", missing)
}
}
// Prüfung 2: absichtlich simulierter Breaking Change (Feld "ObjectType"
// umbenannt, z.B. fälschlich zu "ObjType") lässt den Vertragstest
// fehlschlagen.
func TestContract_GOV01_DetectsBreakingChange_RenamedField(t *testing.T) {
brokenResponse := []byte(`{"TenantSlug":"mandant-a","ModuleName":"dms","ObjType":"document","Fields":[]}`) // "ObjectType" absichtlich umbenannt
required := []string{"TenantSlug", "ModuleName", "ObjectType", "Fields"}
missing, err := RequireJSONFields(brokenResponse, required)
if err != nil {
t.Fatalf("pruefung selbst fehlgeschlagen: %v", err)
}
if len(missing) != 1 || missing[0] != "ObjectType" {
t.Fatalf("erwartet erkanntes umbenanntes feld 'ObjectType', habe: %v", missing)
}
}
// --- Vertrag 2: GOV-01 Feld-Schema (moduletype.Field, JSON-Persistenz) ---
// Akzeptanzkriterium 1 + Prüfung 1.
// TestContract_GOV01_FieldSchema prüft die TATSÄCHLICHE JSON-Kodierung von
// moduletype.Field — das Schema, mit dem DMS/Mail ihre Feldlisten an
// Register übergeben und über Get/ListByModule zurückerhalten.
func TestContract_GOV01_FieldSchema(t *testing.T) {
encoded, err := json.Marshal(moduletype.Field{Name: "title", Kind: "text"})
if err != nil {
t.Fatalf("field kodieren: %v", err)
}
required := []string{"name", "kind"}
missing, err := RequireJSONFields(encoded, required)
if err != nil {
t.Fatalf("antwort nicht lesbar: %v", err)
}
if len(missing) != 0 {
t.Fatalf("field-schema fehlen pflichtfelder: %v", missing)
}
}
// Prüfung 2: absichtlich simulierter Breaking Change (Feld "kind" entfernt).
func TestContract_GOV01_DetectsBreakingChange_RemovedField(t *testing.T) {
brokenField := []byte(`{"name":"title"}`) // "kind" fehlt absichtlich
required := []string{"name", "kind"}
missing, err := RequireJSONFields(brokenField, required)
if err != nil {
t.Fatalf("pruefung selbst fehlgeschlagen: %v", err)
}
if len(missing) != 1 || missing[0] != "kind" {
t.Fatalf("erwartet erkanntes fehlendes feld 'kind', habe: %v", missing)
}
}