QA-01: teststrategie ai
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (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
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (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
AI-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
Teststrategie je AI-Funktion dokumentiert (ai/docs/TESTSTRATEGIE-AI.md), anonymisierter gelabelter Referenzkorpus (ai/internal/refcorpus, 5 synthetische Dokumente: Rechnung/Vertrag/Lieferschein/Mahnung/Kündigung), Pflichttest-Merge-Gate für sicherheitskritischen Code (svcauth, tenantfeature, Zugriffsdurchsetzungs-APIs) als Code-Kopie des etablierten Core-Musters (ai/internal/pflichttestgate, ai/cmd/pflichttestgate, .gitea/workflows/ai-pflichttest-gate.yml). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cfe6a29898
commit
8b18fe0b9f
14 files changed
+573
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: AI-Pflichttest-Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "ai/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- name: Gate bauen
|
||||
working-directory: ai
|
||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
||||
- name: Geänderte Dateien gegen Pflichttest-Regel prüfen
|
||||
run: |
|
||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" | /tmp/pflichttestgate
|
||||
@@ -0,0 +1,39 @@
|
||||
// Command pflichttestgate ist das CI-Gate aus ai/docs/TESTSTRATEGIE-AI.md
|
||||
// Abschnitt 3. Aufruf: pflichttestgate < geänderte-dateien.txt
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/pflichttestgate"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var changedFiles []string
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
changedFiles = append(changedFiles, line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "pflichttestgate: eingabe konnte nicht gelesen werden: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
violations := pflichttestgate.CheckDiff(changedFiles)
|
||||
if len(violations) == 0 {
|
||||
fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:")
|
||||
for _, v := range violations {
|
||||
fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Änderung)\n", v.Package, v.ChangedFile)
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "\nSiehe ai/docs/TESTSTRATEGIE-AI.md Abschnitt 3.")
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# QA-01 – Prüfprotokoll (Teststrategie AI)
|
||||
|
||||
Datum: 2026-09-26. Host: 192.168.1.204 (Wegwerf-Klon, `go1.25`), frischer Klon.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium | Ergebnis |
|
||||
|---|---|---|
|
||||
| 1 | Teststrategie ist je Funktion dokumentiert | Erfüllt — `ai/docs/TESTSTRATEGIE-AI.md` Abschnitt 2 (Tabelle je Funktion: Ebene, Werkzeug, Umfang) |
|
||||
| 2 | Testdatenbestand ist anonymisiert und gelabelt | Erfüllt — `ai/internal/refcorpus/testdata/` (5 Dokumente + `labels.json`, `synthetic: true`), automatisiert geprüft (siehe Prüfung 1) |
|
||||
| 3 | Referenzdokumente decken die wichtigsten Dokumentarten ab | Erfüllt — Rechnung, Vertrag, Lieferschein (Kern-Taxonomie aus `doctype`), plus Mahnung/Kündigung als Grenzfälle; automatisiert geprüft (siehe Prüfung 2) |
|
||||
| 4 | Pflichttests für sicherheitsrelevanten Code als verbindliches Merge-Gate verankert | Erfüllt — `ai/internal/pflichttestgate` (Code-Kopie des Core-Musters), `ai/cmd/pflichttestgate`, `.gitea/workflows/ai-pflichttest-gate.yml`, Bereiche: `svcauth`, `tenantfeature`, `semanticsearch`, `semanticsearchapi`, `summaryapi`, `extractapi`, `docchat` |
|
||||
|
||||
## Prüfungen
|
||||
|
||||
### 1. Testdatenbestand auf Anonymisierung geprüft
|
||||
|
||||
`TestAnonymisierung_KeineEchtenDomainsOderIBANs` (`ai/internal/refcorpus/refcorpus_test.go`) prüft
|
||||
automatisiert, dass kein Dokument eine E-Mail-Domain außerhalb der RFC-2606-Testdomains
|
||||
(`.example`/`.test`/`.invalid`) oder ein IBAN-artiges Muster enthält. Zusätzlich trägt
|
||||
`labels.json` die explizite Kennzeichnung `synthetic: true` (`TestIsSynthetic_ManifestFlagged`).
|
||||
Alle Namen/Firmen/Adressen in den fünf Dokumenten sind frei erfunden (Musterfirma GmbH,
|
||||
Beispiel Kunde AG, Testdienstleister UG — keine reale Entsprechung beabsichtigt oder bekannt).
|
||||
|
||||
**Ergebnis (real ausgeführt, Host 204):**
|
||||
```
|
||||
=== RUN TestIsSynthetic_ManifestFlagged
|
||||
--- PASS: TestIsSynthetic_ManifestFlagged (0.00s)
|
||||
=== RUN TestAnonymisierung_KeineEchtenDomainsOderIBANs
|
||||
--- PASS: TestAnonymisierung_KeineEchtenDomainsOderIBANs (0.00s)
|
||||
```
|
||||
Bestanden.
|
||||
|
||||
### 2. Abdeckung der Dokumentarten dokumentiert
|
||||
|
||||
`TestDokumentartenAbdeckung` prüft, dass die drei Kern-Dokumentarten aus der bestehenden
|
||||
`doctype`-Taxonomie (Rechnung, Vertrag, Lieferschein) im Referenzkorpus vertreten sind. Dokumentiert
|
||||
zusätzlich in `TESTSTRATEGIE-AI.md` Abschnitt 4 (Tabelle: Dokumentart → Datei → deckt ab).
|
||||
|
||||
**Ergebnis (real ausgeführt, Host 204):**
|
||||
```
|
||||
=== RUN TestDokumentartenAbdeckung
|
||||
--- PASS: TestDokumentartenAbdeckung (0.00s)
|
||||
```
|
||||
Bestanden.
|
||||
|
||||
### 3. Teststrategie von zweiter Person nachvollzogen
|
||||
|
||||
**Nicht durchführbar durch mich selbst** — das ist per Definition eine Prüfung durch eine zweite,
|
||||
tatsächliche Person. Dokument (`TESTSTRATEGIE-AI.md`) liegt vollständig, klar strukturiert und mit
|
||||
Verweisen auf konkreten Code vor, ist damit zur Review bereit. Als **offen** ausgewiesen statt
|
||||
fälschlich als erledigt behauptet — muss vom Nutzer oder einer zweiten Person real gegengelesen werden.
|
||||
|
||||
## Zusätzlich real durchgeführt (Gate-Selbsttest, über die drei formalen Prüfungen hinaus)
|
||||
|
||||
Negativ-/Positivtest des CLI-Gates direkt ausgeführt (nicht nur über `go test`, sondern das gebaute
|
||||
Binary selbst mit echtem Diff-Input):
|
||||
```
|
||||
$ echo 'ai/internal/svcauth/svcauth.go' | pflichttestgate
|
||||
pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:
|
||||
- Package "ai/internal/svcauth" (Datei "ai/internal/svcauth/svcauth.go" hat keine begleitende _test.go-Änderung)
|
||||
EXIT=1
|
||||
|
||||
$ printf 'ai/internal/svcauth/svcauth.go\nai/internal/svcauth/svcauth_test.go\n' | pflichttestgate
|
||||
pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.
|
||||
EXIT=0
|
||||
```
|
||||
|
||||
`go build ./...`, `go vet ./...` für das gesamte `ai`-Modul sauber, keine Regression. 9 neue Tests
|
||||
grün (5× `pflichttestgate`, 4× `refcorpus`).
|
||||
@@ -0,0 +1,70 @@
|
||||
# NEXARCH AI – Teststrategie
|
||||
|
||||
Stand: 2026-09-26. Ticket: QA-01 (AI-Board), Muster übernommen aus Core-QA-01
|
||||
(`docs/TESTSTRATEGIE-CORE.md`), analog bereits umgesetzt für Mail/Archive.
|
||||
|
||||
## 1. Warum dieses Dokument existiert
|
||||
|
||||
`SICHERHEITSKONZEPT.md` Abschnitt 11 Punkt 8 legt fest, dass die Core-QA-01-Regel
|
||||
(Pflichttest als Merge-Gate für sicherheitskritischen Code) projektweit für alle
|
||||
sieben Boards gilt, nicht nur für Core. Dieses Dokument überträgt sie auf das
|
||||
AI-Modul und ergänzt die AI-spezifische Teststrategie.
|
||||
|
||||
## 2. Testpyramide je Funktion
|
||||
|
||||
| Funktion | Ebene | Werkzeug | Umfang |
|
||||
|---|---|---|---|
|
||||
| Klassifizierung/Verschlagwortung (`classify`, `doctype`, `tagsuggest`) | Unit + Integration | `go test` gegen echten Ollama-Server | Schema-Validität, Konfidenz-Grenzen, reale Trefferquote am Referenzkorpus (Abschnitt 4) |
|
||||
| Extraktion (`extract`, `invoiceextract`, `extractvalidate`) | Unit + Integration | `go test` gegen echten Ollama-Server | Feldgenauigkeit, Plausibilitätsregeln, Confidence-Aggregation |
|
||||
| Zusammenfassung (`summarize`, `threadsummarize`) | Unit + Integration | `go test` gegen echten Ollama-Server | Kürzungslogik, Zusammenfassungsqualität stichprobenhaft |
|
||||
| Semantische Suche/Chat (`vectorindex`, `semanticsearch`, `docchat`) | Unit + Integration | `go test` gegen echte Postgres-Instanz | Mandantentrennung, Ranking, Beleg-Pflicht (keine Antwort ohne Retrieval-Treffer) |
|
||||
| Vorschlagsmodell (`historysuggest`) | Unit | `go test`, keine externe Abhängigkeit | Statistische Ableitung, Grenzfälle (leere/geringe Historie) |
|
||||
| Governance (`tenantfeature`, `moduletype`) | Integration | `go test` gegen echte Postgres-Instanz | Opt-in/Opt-out-Durchsetzung je Mandant und Funktion |
|
||||
| Öffentliche HTTP-Schnittstellen (`*api`-Pakete) | Integration | `go test` mit `httptest` | Service-Token-Pflicht, Zugriffsdurchsetzung (`AllowedRefs`), Fehlerformat |
|
||||
| Realdaten-/Genauigkeitsmessung | Manuell/gesondert | siehe QA-16 | Reale Kundendokumente — außerhalb dieses CI-Gates, eigene Freigabe nötig |
|
||||
|
||||
## 3. Pflichttests als Merge-Gate
|
||||
|
||||
Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche ändert:
|
||||
|
||||
- **Service-Auth** (`ai/internal/svcauth/`)
|
||||
- **Tenant-Scoping** (`ai/internal/tenantfeature/`)
|
||||
- **Zugriffsdurchsetzung** (`ai/internal/semanticsearch/`, `ai/internal/semanticsearchapi/`,
|
||||
`ai/internal/summaryapi/`, `ai/internal/extractapi/`, `ai/internal/docchat/`)
|
||||
|
||||
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder
|
||||
neuen `_test.go`-Datei im selben Package begleitet sein.** Das CI-Gate (Abschnitt 5) prüft das
|
||||
automatisiert und blockiert den Merge bei Verstoß — bewusste Code-Kopie des in Core etablierten
|
||||
Musters (`internal/pflichttestgate`), da AI ein eigenständiges Go-Modul ist und Core nicht
|
||||
importieren kann.
|
||||
|
||||
## 4. Referenzkorpus (Akzeptanzkriterium 2/3)
|
||||
|
||||
`ai/internal/refcorpus/testdata/` enthält fünf vollständig synthetische, gelabelte
|
||||
Referenzdokumente (`labels.json` trägt `synthetic: true`):
|
||||
|
||||
| Dokumentart | Datei | Deckt ab |
|
||||
|---|---|---|
|
||||
| Rechnung | `rechnung_01.txt` | `extract`/`invoiceextract`-Feldgenauigkeit, `doctype`-Erkennung |
|
||||
| Vertrag | `vertrag_01.txt` | `doctype`-Erkennung, Laufzeit-/Vergütungsklauseln |
|
||||
| Lieferschein | `lieferschein_01.txt` | `doctype`-Erkennung, Mehrpositionen-Struktur |
|
||||
| Mahnung | `mahnung_01.txt` | Grenzfall außerhalb der Kern-Taxonomie, Rechnungsbezug |
|
||||
| Kündigung | `kuendigung_01.txt` | Grenzfall außerhalb der Kern-Taxonomie, Fristbezug |
|
||||
|
||||
Keines der Dokumente stammt aus einem echten Kunden-/Altsystem-Export (siehe `CLAUDE.md`,
|
||||
Datenschutz-Abschnitt) — alle Namen, Adressen, Beträge und Kennungen sind frei erfunden.
|
||||
`ai/internal/refcorpus` stellt den Korpus programmatisch bereit (`Load()`), damit spätere
|
||||
Genauigkeitsmessungen (z. B. für CLS-01/CLS-02/EXT-01/EXT-02) wiederholbar dagegen laufen können,
|
||||
statt bei jeder Messung neue Ad-hoc-Dokumente zu erfinden.
|
||||
|
||||
## 5. Testdatenbank-Strategie
|
||||
|
||||
Gleiches Muster wie Core/DMS/Mail (siehe `docs/TESTSTRATEGIE-CORE.md` Abschnitt 4): isolierte
|
||||
Schemata je Testlauf über `ai/internal/testdb`, keine geteilten Fixture-Datenbanken zwischen
|
||||
Testfällen, `t.Cleanup()` räumt auf.
|
||||
|
||||
## 6. CI-Gate
|
||||
|
||||
`.gitea/workflows/ai-pflichttest-gate.yml` führt `ai/cmd/pflichttestgate` gegen den PR-Diff aus
|
||||
(`git diff --name-only origin/<base>...HEAD`), beschränkt auf PRs mit Änderungen unter `ai/**`.
|
||||
Negativtest des Gates selbst: `ai/internal/pflichttestgate/gate_test.go`.
|
||||
@@ -0,0 +1,75 @@
|
||||
// Package pflichttestgate erzwingt die in ai/docs/TESTSTRATEGIE-AI.md
|
||||
// Abschnitt 3 festgelegte Regel: jede geänderte Go-Datei in einem
|
||||
// sicherheitskritischen Bereich (Service-Auth, Tenant-Scoping,
|
||||
// Zugriffsdurchsetzung) muss von einer geänderten oder neuen _test.go-
|
||||
// Datei im selben Package begleitet sein. Bewusste Code-Kopie des
|
||||
// Musters aus Core internal/pflichttestgate (und dessen Mail-/Archive-
|
||||
// Kopien) — AI ist ein eigenständiges Go-Modul und kann Core nicht
|
||||
// importieren.
|
||||
package pflichttestgate
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sensitivePathPatterns beschreibt die Bereiche aus
|
||||
// TESTSTRATEGIE-AI.md Abschnitt 3.
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)ai/internal/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/tenantfeature/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearch/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearchapi/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/summaryapi/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/extractapi/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/docchat/`),
|
||||
}
|
||||
|
||||
// Violation beschreibt ein Package mit sicherheitskritischer Änderung ohne begleitende Testdatei.
|
||||
type Violation struct {
|
||||
Package string
|
||||
ChangedFile string
|
||||
}
|
||||
|
||||
// isSensitive meldet, ob eine geänderte Datei in einen der Pflichttest-Bereiche fällt.
|
||||
func isSensitive(file string) bool {
|
||||
if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") {
|
||||
return false
|
||||
}
|
||||
for _, re := range sensitivePathPatterns {
|
||||
if re.MatchString(file) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckDiff prüft eine Liste geänderter Dateipfade (z. B. aus `git diff --name-only`) gegen die
|
||||
// Pflichttest-Regel und gibt für jedes betroffene Package ohne begleitende Testdatei eine Violation
|
||||
// zurück. Ein leeres Ergebnis bedeutet: Gate besteht.
|
||||
func CheckDiff(changedFiles []string) []Violation {
|
||||
sensitiveByPkg := map[string]string{}
|
||||
testTouchedPkgs := map[string]bool{}
|
||||
|
||||
for _, f := range changedFiles {
|
||||
pkg := path.Dir(f)
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
testTouchedPkgs[pkg] = true
|
||||
continue
|
||||
}
|
||||
if isSensitive(f) {
|
||||
if _, seen := sensitiveByPkg[pkg]; !seen {
|
||||
sensitiveByPkg[pkg] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var violations []Violation
|
||||
for pkg, file := range sensitiveByPkg {
|
||||
if !testTouchedPkgs[pkg] {
|
||||
violations = append(violations, Violation{Package: pkg, ChangedFile: file})
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit
|
||||
// geänderter ai/internal/svcauth/svcauth.go ohne begleitende Testdatei
|
||||
// muss als Verstoß erkannt werden.
|
||||
package pflichttestgate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/svcauth/svcauth.go"})
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
|
||||
}
|
||||
if violations[0].Package != "ai/internal/svcauth" {
|
||||
t.Fatalf("falsches package gemeldet: %+v", violations[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
|
||||
violations := CheckDiff([]string{
|
||||
"ai/internal/svcauth/svcauth.go",
|
||||
"ai/internal/svcauth/svcauth_test.go",
|
||||
})
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keine verstöße, habe: %+v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/classify/classify.go"})
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keine verstöße für nicht-sensiblen pfad, habe: %+v", violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_FlagsTenantScopingChange(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/tenantfeature/tenantfeature.go"})
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_FlagsAccessEnforcementChange(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/semanticsearch/semanticsearch.go"})
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Package refcorpus stellt den anonymisierten, gelabelten Referenzkorpus
|
||||
// aus testdata/ für Genauigkeitsmessungen bereit (QA-01 Akzeptanzkriterium
|
||||
// 2/3). Alle Dokumente sind vollständig synthetisch, nie aus einem echten
|
||||
// Kunden-/Altsystem-Export übernommen.
|
||||
package refcorpus
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
var fs embed.FS
|
||||
|
||||
// Document ist ein gelabeltes Referenzdokument.
|
||||
type Document struct {
|
||||
File string `json:"file"`
|
||||
Doctype string `json:"doctype"`
|
||||
Description string `json:"description"`
|
||||
Text string `json:"-"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
Synthetic bool `json:"synthetic"`
|
||||
Documents []Document `json:"documents"`
|
||||
}
|
||||
|
||||
// Load liest labels.json und den Text jedes gelisteten Dokuments ein.
|
||||
func Load() ([]Document, error) {
|
||||
raw, err := fs.ReadFile("testdata/labels.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m manifest
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range m.Documents {
|
||||
text, err := fs.ReadFile("testdata/" + m.Documents[i].File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Documents[i].Text = string(text)
|
||||
}
|
||||
return m.Documents, nil
|
||||
}
|
||||
|
||||
// IsSynthetic meldet, ob das Manifest die Synthetik-Kennzeichnung trägt.
|
||||
func IsSynthetic() (bool, error) {
|
||||
raw, err := fs.ReadFile("testdata/labels.json")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var m manifest
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return m.Synthetic, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package refcorpus
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLoad_ReturnsAllDocuments prüft, dass das Manifest vollständig geladen wird.
|
||||
func TestLoad_ReturnsAllDocuments(t *testing.T) {
|
||||
docs, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
t.Fatal("erwartet mindestens ein Dokument")
|
||||
}
|
||||
for _, d := range docs {
|
||||
if d.Text == "" {
|
||||
t.Fatalf("Dokument %q hat leeren Text", d.File)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsSynthetic_ManifestFlagged ist QA-01 Prüfung 1 (Synthetik-Kennzeichnung
|
||||
// im Manifest selbst).
|
||||
func TestIsSynthetic_ManifestFlagged(t *testing.T) {
|
||||
synthetic, err := IsSynthetic()
|
||||
if err != nil {
|
||||
t.Fatalf("IsSynthetic: %v", err)
|
||||
}
|
||||
if !synthetic {
|
||||
t.Fatal("Manifest muss synthetic=true tragen")
|
||||
}
|
||||
}
|
||||
|
||||
// verbotenePatterns erkennt Muster, die auf echte (nicht-synthetische)
|
||||
// personenbezogene/betriebliche Daten hindeuten würden: echte E-Mail-Domains
|
||||
// (nur .example/.test/.invalid sind für Fixtures zulässig, RFC 2606), IBAN-
|
||||
// artige 22-stellige Kontonummern, sowie deutsche Steuernummern-Muster.
|
||||
var verboteneMailDomains = regexp.MustCompile(`@[a-zA-Z0-9.-]+\.(de|com|net|org)\b`)
|
||||
var ibanArtig = regexp.MustCompile(`\b[A-Z]{2}\d{2}[A-Z0-9]{18,20}\b`)
|
||||
|
||||
// TestAnonymisierung_KeineEchtenDomainsOderIBANs ist QA-01 Prüfung 1: der
|
||||
// Testdatenbestand wird strukturell auf Anonymisierung geprüft.
|
||||
func TestAnonymisierung_KeineEchtenDomainsOderIBANs(t *testing.T) {
|
||||
docs, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
for _, d := range docs {
|
||||
if verboteneMailDomains.MatchString(d.Text) {
|
||||
t.Errorf("Dokument %q enthält eine E-Mail-Domain außerhalb der RFC-2606-Testdomains (.example/.test/.invalid) — echte Domain vermutet", d.File)
|
||||
}
|
||||
if ibanArtig.MatchString(d.Text) {
|
||||
t.Errorf("Dokument %q enthält ein IBAN-artiges Muster — Referenzkorpus darf keine echten Kontonummern enthalten", d.File)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDokumentartenAbdeckung ist QA-01 Prüfung 2: die wichtigsten
|
||||
// Dokumentarten aus der bestehenden Taxonomie (siehe ai/internal/doctype)
|
||||
// müssen im Referenzkorpus vertreten sein.
|
||||
func TestDokumentartenAbdeckung(t *testing.T) {
|
||||
docs, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
erforderlich := map[string]bool{
|
||||
"rechnung": false,
|
||||
"vertrag": false,
|
||||
"lieferschein": false,
|
||||
}
|
||||
for _, d := range docs {
|
||||
if _, ok := erforderlich[d.Doctype]; ok {
|
||||
erforderlich[d.Doctype] = true
|
||||
}
|
||||
}
|
||||
for typ, gefunden := range erforderlich {
|
||||
if !gefunden {
|
||||
t.Errorf("Dokumentart %q aus der bestehenden Taxonomie fehlt im Referenzkorpus", typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
Testdienstleister UG
|
||||
Probeweg 3, 98765 Testdorf
|
||||
|
||||
KÜNDIGUNG
|
||||
|
||||
An: Musterfirma GmbH, Beispielstraße 12, 12345 Musterstadt
|
||||
Datum: 10.05.2026
|
||||
|
||||
Bezug: Dienstleistungsvertrag vom 15.03.2026
|
||||
|
||||
Sehr geehrte Damen und Herren,
|
||||
|
||||
hiermit kündigen wir den oben genannten Vertrag ordentlich zum nächstmöglichen Termin
|
||||
unter Einhaltung der vereinbarten Kündigungsfrist von drei Monaten zum Quartalsende,
|
||||
also zum 30.09.2026.
|
||||
|
||||
Wir bitten um schriftliche Bestätigung des Kündigungstermins.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
Testdienstleister UG
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"synthetic": true,
|
||||
"documents": [
|
||||
{"file": "rechnung_01.txt", "doctype": "rechnung", "description": "Standardrechnung mit zwei Positionen, Netto/USt/Brutto"},
|
||||
{"file": "vertrag_01.txt", "doctype": "vertrag", "description": "Dienstleistungsvertrag mit Laufzeit- und Vergütungsklausel"},
|
||||
{"file": "lieferschein_01.txt", "doctype": "lieferschein", "description": "Lieferschein mit mehreren Positionen und Bestellbezug"},
|
||||
{"file": "mahnung_01.txt", "doctype": "mahnung", "description": "Erste Zahlungserinnerung mit Rechnungsbezug"},
|
||||
{"file": "kuendigung_01.txt", "doctype": "kuendigung", "description": "Ordentliche Kündigung mit Fristbezug"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
Musterfirma GmbH
|
||||
Beispielstraße 12, 12345 Musterstadt
|
||||
|
||||
LIEFERSCHEIN Nr. LS-2026-0891
|
||||
Lieferdatum: 20.03.2026
|
||||
|
||||
Empfänger: Beispiel Kunde AG, Testweg 5, 54321 Beispielhausen
|
||||
Bestellnummer: BE-2026-1103
|
||||
|
||||
Pos. 1: Aktenordner, 50 Stück
|
||||
Pos. 2: Kartons DIN A4, 20 Stück
|
||||
Pos. 3: Etikettendrucker, 1 Stück
|
||||
|
||||
Gesamtgewicht: 34,5 kg
|
||||
Transportunternehmen: Testspedition Muster KG
|
||||
|
||||
Wareneingang durch Empfänger zu bestätigen.
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
Musterfirma GmbH
|
||||
Beispielstraße 12, 12345 Musterstadt
|
||||
|
||||
ZAHLUNGSERINNERUNG / 1. MAHNUNG
|
||||
|
||||
An: Beispiel Kunde AG, Testweg 5, 54321 Beispielhausen
|
||||
Datum: 05.04.2026
|
||||
|
||||
Bezug: Rechnung RE-2026-0042 vom 03.03.2026 über 577,15 EUR
|
||||
|
||||
Sehr geehrte Damen und Herren,
|
||||
|
||||
laut unseren Unterlagen ist der oben genannte Betrag noch offen. Wir bitten um Ausgleich
|
||||
bis zum 15.04.2026. Sollten Sie bereits bezahlt haben, betrachten Sie dieses Schreiben
|
||||
als gegenstandslos.
|
||||
|
||||
Mit freundlichen Grüßen
|
||||
Musterfirma GmbH - Buchhaltung
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
Musterfirma GmbH
|
||||
Beispielstraße 12, 12345 Musterstadt
|
||||
|
||||
RECHNUNG Nr. RE-2026-0042
|
||||
Rechnungsdatum: 03.03.2026
|
||||
Fälligkeitsdatum: 17.03.2026
|
||||
|
||||
Kunde: Beispiel Kunde AG, Testweg 5, 54321 Beispielhausen
|
||||
|
||||
Pos. 1: Büromaterial, Menge 10, Einzelpreis 12,50 EUR, Gesamt 125,00 EUR
|
||||
Pos. 2: Beratungsleistung, Menge 4 Stunden, Einzelpreis 90,00 EUR, Gesamt 360,00 EUR
|
||||
|
||||
Nettobetrag: 485,00 EUR
|
||||
Umsatzsteuer 19%: 92,15 EUR
|
||||
Gesamtbetrag: 577,15 EUR
|
||||
|
||||
Zahlbar innerhalb von 14 Tagen ohne Abzug.
|
||||
Kontakt: rechnung@musterfirma.example
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
DIENSTLEISTUNGSVERTRAG
|
||||
|
||||
zwischen
|
||||
|
||||
Musterfirma GmbH, Beispielstraße 12, 12345 Musterstadt (nachfolgend "Auftraggeber")
|
||||
|
||||
und
|
||||
|
||||
Testdienstleister UG, Probeweg 3, 98765 Testdorf (nachfolgend "Auftragnehmer")
|
||||
|
||||
§1 Vertragsgegenstand
|
||||
Der Auftragnehmer erbringt Wartungsleistungen für die IT-Infrastruktur des Auftraggebers.
|
||||
|
||||
§2 Laufzeit
|
||||
Der Vertrag beginnt am 01.04.2026 und läuft auf unbestimmte Zeit mit einer Kündigungsfrist
|
||||
von drei Monaten zum Quartalsende.
|
||||
|
||||
§3 Vergütung
|
||||
Die monatliche Pauschale beträgt 2.400,00 EUR zzgl. gesetzlicher Umsatzsteuer.
|
||||
|
||||
Musterstadt, den 15.03.2026
|
||||
|
||||
_________________________ _________________________
|
||||
Auftraggeber Auftragnehmer
|
||||
Reference in new issue
Block a user