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

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:
patrickandClaude Sonnet 5 committed 2026-09-26 16:07:44 +02:00
1 parent cfe6a29898
commit 8b18fe0b9f
14 files changed
+573

No files matched your search

+23
View File
@@ -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
+39
View File
@@ -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)
}
+70
View File
@@ -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`).
+70
View File
@@ -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`.
+75
View File
@@ -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
}
+47
View File
@@ -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)
}
}
+59
View File
@@ -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
}
+83
View File
@@ -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)
}
}
}
+20
View File
@@ -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
View File
@@ -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"}
]
}
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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