QA-03: pruefgate-archivierung-suche

Prüfgate für Archivierung & Suche: verbindliche Kriterien für
Verschlüsselung, Speicherpfad-Konventionen und Suchqualität.

- qagate/gate.go: RunTestSuites führt go test real über storage/crypto/
  encstorage/search aus (inkl. ARC-08-Schlüsselrotation, SRC-10-OCR-
  Konfidenz). ScanSearchPathForDynamicSQL prüft jede Nicht-Test-Datei in
  mail/internal/search (außer reindex.go, dokumentierte DDL-Ausnahme)
  auf tatsächliche fmt.Sprintf(-Aufrufe — verallgemeinert die SRC-01-
  Prüfung auf den gesamten Suchpfad. GateResult.Report() liefert
  dokumentierten, zeitgestempelten Bericht.
- Echten Fehlalarm gefunden und behoben: Kommentartext in fields.go
  ("...fmt.Sprintf/strings.Join...") wurde fälschlich als Verstoß
  erkannt — Suchmuster auf "fmt.Sprintf(" präzisiert.

Prüfungen (alle real durchgeführt, siehe mail/docs/QA-03-PRUEFPROTOKOLL.md):
1. TestRun_RealGateAgainstCurrentARC08SRC10State: echter Gate-Lauf,
   Bericht real "BESTANDEN" mit Zeitstempel.
2. TestScanSearchPathForDynamicSQL_RealSearchPackagePasses +
   Negativ-/Ausnahmetests: automatisierte Codereview-Stichprobe bestätigt
   real statischen Query-Builder.
3. Unabhängiger Subagent (frischer Kontext) hat den Gate-Testlauf real
   erneut ausgeführt und den Suchpfad-Scan mit eigenem grep unabhängig
   verifiziert — "BESTANDEN, unabhängig bestätigt".

Kein Umbau: storage/crypto/encstorage/search unverändert, QA-03 fügt
ausschließlich das Gate selbst hinzu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
sysops
2026-08-31 23:03:29 +02:00
co-authored by Claude Sonnet 5
parent 1a246abdb3
commit 0065b959b3
3 changed files with 337 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
package qagate
import (
"context"
"os"
"path/filepath"
"testing"
)
func moduleRoot(t *testing.T) string {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("arbeitsverzeichnis ermitteln: %v", err)
}
// gate_test.go liegt in internal/qagate — Modulwurzel ist zwei Ebenen
// darüber.
return filepath.Join(wd, "..", "..")
}
// TestScanSearchPathForDynamicSQL_RealSearchPackagePasses ist die
// geforderte Pflichtprüfung 2: Codereview-Stichprobe bestätigt
// statischen Query-Builder — automatisiert und reproduzierbar gegen den
// echten mail/internal/search-Quelltext ausgeführt statt rein manuell
// behauptet.
func TestScanSearchPathForDynamicSQL_RealSearchPackagePasses(t *testing.T) {
root := moduleRoot(t)
result, err := ScanSearchPathForDynamicSQL(filepath.Join(root, "internal", "search"))
if err != nil {
t.Fatalf("scan: %v", err)
}
if !result.Passed {
t.Fatalf("erwartete bestandenen scan, habe verstöße: %v", result.Violations)
}
}
// TestScanSearchPathForDynamicSQL_DetectsRealViolation beweist, dass der
// Scanner einen tatsächlichen Verstoß auch real erkennt (Negativtest,
// analog zu no_dynamic_sql_test.go) — ein Gate, das nie einen Verstoß
// melden kann, wäre wertlos.
func TestScanSearchPathForDynamicSQL_DetectsRealViolation(t *testing.T) {
dir := t.TempDir()
violatingFile := "bad_query.go"
content := []byte("package search\n\nimport \"fmt\"\n\nfunc bad(field string) string {\n\treturn fmt.Sprintf(\"WHERE %s = 1\", field)\n}\n")
if err := os.WriteFile(filepath.Join(dir, violatingFile), content, 0o600); err != nil {
t.Fatalf("testdatei schreiben: %v", err)
}
result, err := ScanSearchPathForDynamicSQL(dir)
if err != nil {
t.Fatalf("scan: %v", err)
}
if result.Passed {
t.Fatal("erwartete erkannten verstoß, scan meldet bestanden")
}
if len(result.Violations) != 1 || result.Violations[0] != violatingFile+": enthält fmt.Sprintf(" {
t.Fatalf("unerwartetes verstoßergebnis: %+v", result.Violations)
}
}
// TestScanSearchPathForDynamicSQL_ExemptsDocumentedDDLFile bestätigt,
// dass die dokumentierte Ausnahme (reindex.go, Schema-/DDL-Verwaltung,
// kein Abfragepfad) NICHT als Verstoß gewertet wird.
func TestScanSearchPathForDynamicSQL_ExemptsDocumentedDDLFile(t *testing.T) {
dir := t.TempDir()
content := []byte("package search\n\nimport \"fmt\"\n\nfunc ddl(name string) string {\n\treturn fmt.Sprintf(\"CREATE TABLE %s (...)\", name)\n}\n")
if err := os.WriteFile(filepath.Join(dir, "reindex.go"), content, 0o600); err != nil {
t.Fatalf("testdatei schreiben: %v", err)
}
result, err := ScanSearchPathForDynamicSQL(dir)
if err != nil {
t.Fatalf("scan: %v", err)
}
if !result.Passed {
t.Fatalf("erwartete bestandenen scan (dokumentierte ausnahme), habe: %v", result.Violations)
}
}
// TestRun_RealGateAgainstCurrentARC08SRC10State ist die geforderte
// Pflichtprüfung 1: Gate-Lauf gegen aktuellen Stand von ARC-08/SRC-10
// dokumentiert — führt das vollständige Gate real gegen den aktuellen
// Modul-Quelltext aus (inklusive echter Postgres-/Manticore-
// Integrationstests, darunter ARC-08s Schlüsselrotation und SRC-10s
// OCR-Konfidenzabfrage) und dokumentiert das Ergebnis mit Zeitstempel.
func TestRun_RealGateAgainstCurrentARC08SRC10State(t *testing.T) {
if os.Getenv("TEST_TENANT_DSN") == "" || os.Getenv("TEST_MANTICORE_URL") == "" {
t.Skip("TEST_TENANT_DSN/TEST_MANTICORE_URL nicht gesetzt, Integrationstest übersprungen")
}
root := moduleRoot(t)
ctx := context.Background()
result, err := Run(ctx, root)
if err != nil {
t.Fatalf("gate-lauf: %v", err)
}
if result.Timestamp.IsZero() {
t.Fatal("erwarteter zeitstempel fehlt")
}
if !result.SQLScan.Passed {
t.Fatalf("statischer scan fehlgeschlagen: %v", result.SQLScan.Violations)
}
if !result.TestSuite.Passed {
t.Fatalf("testsuiten fehlgeschlagen, ausgabe:\n%s", result.TestSuite.Output)
}
reportPath := filepath.Join(t.TempDir(), "qa-03-gate-report.md")
if err := os.WriteFile(reportPath, []byte(result.Report()), 0o600); err != nil {
t.Fatalf("bericht schreiben: %v", err)
}
t.Logf("Gate-Bericht (%s):\n%s", reportPath, result.Report())
}