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
163 lines
5.7 KiB
Go
163 lines
5.7 KiB
Go
// Package qagate implementiert QA-03: das Prüfgate für Archivierung &
|
|
// Suche. Verbindliche Kriterien: (1) die Archivierungs- und
|
|
// Suchindex-Testsuiten bestehen, einschließlich Schlüsselrotation
|
|
// (ARC-08), (2) der Suchpfad baut keine SQL-Klauseln dynamisch zusammen,
|
|
// (3) das Gate-Ergebnis ist dokumentiert und mit Zeitstempel
|
|
// nachvollziehbar.
|
|
package qagate
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ArchiveAndSearchPackages sind die Pakete, deren Testsuiten das Gate
|
|
// ausführt (Akzeptanzkriterium 1: Archivierung — storage/crypto/
|
|
// encstorage, inklusive Schlüsselrotation aus ARC-08 — UND Suchindex —
|
|
// search, inklusive Facetten/Reindex/OCR-Konfidenz).
|
|
var ArchiveAndSearchPackages = []string{
|
|
"./internal/storage/...",
|
|
"./internal/crypto/...",
|
|
"./internal/encstorage/...",
|
|
"./internal/search/...",
|
|
}
|
|
|
|
// dynamicSQLBuildMarker ist das Muster, dessen Anwesenheit im Suchpfad
|
|
// (Akzeptanzkriterium 2) einen Verstoß bedeutet — dieselbe Prüfung wie
|
|
// mail/internal/search/no_dynamic_sql_test.go, hier auf alle
|
|
// Suchpfad-Dateien verallgemeinert statt nur auf client.go beschränkt.
|
|
// Der abschließende "(" grenzt einen tatsächlichen Funktionsaufruf von
|
|
// bloßer Erwähnung in Kommentaren ab (z. B. "kein fmt.Sprintf/strings.Join"
|
|
// in Erklärtexten, real als Fehlalarm aufgetreten und hiermit behoben).
|
|
const dynamicSQLBuildMarker = "fmt.Sprintf("
|
|
|
|
// sqlBuildExceptionFiles sind Dateien, die fmt.Sprintf bewusst und
|
|
// dokumentiert für Schema-/DDL-Verwaltung nutzen (Tabellennamen aus
|
|
// paketintern erzeugten, regex-geprüften Werten — SRC-09 reindex.go),
|
|
// NICHT für den eigentlichen Abfrage-/Suchpfad. Diese Unterscheidung
|
|
// steht bereits im Quelltext von reindex.go dokumentiert.
|
|
var sqlBuildExceptionFiles = map[string]bool{
|
|
"reindex.go": true,
|
|
}
|
|
|
|
// TestSuiteResult ist das Ergebnis eines `go test`-Laufs über die
|
|
// Archivierungs-/Suchindex-Pakete.
|
|
type TestSuiteResult struct {
|
|
Passed bool
|
|
Output string
|
|
}
|
|
|
|
// SQLScanResult ist das Ergebnis des statischen Suchpfad-Scans.
|
|
type SQLScanResult struct {
|
|
Passed bool
|
|
Violations []string // "<Datei>: enthält fmt.Sprintf"
|
|
}
|
|
|
|
// GateResult fasst ein vollständiges Gate-Ergebnis zusammen
|
|
// (Akzeptanzkriterium 3: dokumentiert, mit Zeitstempel).
|
|
type GateResult struct {
|
|
Timestamp time.Time
|
|
TestSuite TestSuiteResult
|
|
SQLScan SQLScanResult
|
|
}
|
|
|
|
// Passed ist true, wenn ALLE Kriterien erfüllt sind.
|
|
func (r GateResult) Passed() bool {
|
|
return r.TestSuite.Passed && r.SQLScan.Passed
|
|
}
|
|
|
|
// RunTestSuites führt `go test` über ArchiveAndSearchPackages im
|
|
// angegebenen Modulverzeichnis aus (Akzeptanzkriterium 1). Umgebungs-
|
|
// variablen (z. B. TEST_TENANT_DSN/TEST_MANTICORE_URL für die echten
|
|
// Integrationstests, darunter ARC-08s Schlüsselrotationstests) werden
|
|
// unverändert vom aufrufenden Prozess vererbt — das Gate setzt selbst
|
|
// keine Zugangsdaten.
|
|
func RunTestSuites(ctx context.Context, moduleDir string) (TestSuiteResult, error) {
|
|
args := append([]string{"test", "-count=1"}, ArchiveAndSearchPackages...)
|
|
cmd := exec.CommandContext(ctx, "go", args...)
|
|
cmd.Dir = moduleDir
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if _, isExitErr := err.(*exec.ExitError); !isExitErr {
|
|
return TestSuiteResult{}, fmt.Errorf("qagate: go test ausführen: %w", err)
|
|
}
|
|
}
|
|
return TestSuiteResult{Passed: err == nil, Output: out.String()}, nil
|
|
}
|
|
|
|
// ScanSearchPathForDynamicSQL prüft alle Nicht-Test-.go-Dateien in
|
|
// searchPkgDir auf dynamische SQL-Klauselbildung (Akzeptanzkriterium 2),
|
|
// mit Ausnahme der dokumentierten DDL-Verwaltung (reindex.go).
|
|
func ScanSearchPathForDynamicSQL(searchPkgDir string) (SQLScanResult, error) {
|
|
entries, err := os.ReadDir(searchPkgDir)
|
|
if err != nil {
|
|
return SQLScanResult{}, fmt.Errorf("qagate: suchpfad-verzeichnis lesen: %w", err)
|
|
}
|
|
|
|
var violations []string
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
|
continue
|
|
}
|
|
if sqlBuildExceptionFiles[name] {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(searchPkgDir, name))
|
|
if err != nil {
|
|
return SQLScanResult{}, fmt.Errorf("qagate: %s lesen: %w", name, err)
|
|
}
|
|
if strings.Contains(string(content), dynamicSQLBuildMarker) {
|
|
violations = append(violations, fmt.Sprintf("%s: enthält %s", name, dynamicSQLBuildMarker))
|
|
}
|
|
}
|
|
return SQLScanResult{Passed: len(violations) == 0, Violations: violations}, nil
|
|
}
|
|
|
|
// Run führt das vollständige Gate aus.
|
|
func Run(ctx context.Context, moduleDir string) (GateResult, error) {
|
|
testResult, err := RunTestSuites(ctx, moduleDir)
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
sqlResult, err := ScanSearchPathForDynamicSQL(filepath.Join(moduleDir, "internal", "search"))
|
|
if err != nil {
|
|
return GateResult{}, err
|
|
}
|
|
return GateResult{
|
|
Timestamp: time.Now().UTC(),
|
|
TestSuite: testResult,
|
|
SQLScan: sqlResult,
|
|
}, nil
|
|
}
|
|
|
|
// Report erzeugt den dokumentierten, zeitgestempelten Gate-Bericht
|
|
// (Akzeptanzkriterium 3).
|
|
func (r GateResult) Report() string {
|
|
status := "BESTANDEN"
|
|
if !r.Passed() {
|
|
status = "FEHLGESCHLAGEN"
|
|
}
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# QA-03 Gate-Ergebnis: %s\n\n", status)
|
|
fmt.Fprintf(&b, "Zeitstempel (UTC): %s\n\n", r.Timestamp.Format(time.RFC3339))
|
|
fmt.Fprintf(&b, "## Testsuiten (Archivierung & Suche, inkl. Schlüsselrotation)\n\nBestanden: %v\n\n", r.TestSuite.Passed)
|
|
fmt.Fprintf(&b, "## Statischer Suchpfad-Scan (keine dynamische SQL-Klauselbildung)\n\nBestanden: %v\n", r.SQLScan.Passed)
|
|
if len(r.SQLScan.Violations) > 0 {
|
|
fmt.Fprintf(&b, "Verstöße:\n")
|
|
for _, v := range r.SQLScan.Violations {
|
|
fmt.Fprintf(&b, "- %s\n", v)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|