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
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:
commit
223d07ca96
4 files changed
+219
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user