From ff5113f0b2b510656e3f89a8e4513418a4d9691c Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 27 Oct 2026 23:30:13 +0100 Subject: [PATCH] ai: auditclient, httpclient, loadtest, svcauth, tenantregistryclient aus common v0.1.0 Das Pflichttest-Gate von ai verweist nicht mehr auf das geloeschte svcauth-Paket (common hat ein eigenes Gate). Co-Authored-By: Claude Sonnet 5.5 --- ai/cmd/ai-app/main.go | 2 +- ai/cmd/searchchatapi-a11y-fixture/main.go | 2 +- ai/go.mod | 5 +- ai/go.sum | 2 + ai/internal/auditclient/auditclient.go | 72 --------- ai/internal/auditclient/auditclient_test.go | 48 ------ ai/internal/classify/classify_test.go | 2 +- ai/internal/decisionaudit/decisionaudit.go | 2 +- .../decisionaudit/decisionaudit_test.go | 2 +- .../docchat/docchat_qa08_loadtest_test.go | 2 +- ai/internal/docchatapi/docchatapi.go | 2 +- ai/internal/extractapi/extractapi.go | 2 +- .../extractconfirmapi/extractconfirmapi.go | 2 +- ai/internal/httpclient/httpclient.go | 25 ---- ai/internal/httpclient/httpclient_test.go | 29 ---- ai/internal/loadtest/loadtest.go | 138 ------------------ .../metadatapushclient/metadatapushclient.go | 2 +- .../metadatapushclient_test.go | 2 +- ai/internal/moduletypeapi/moduletypeapi.go | 2 +- ai/internal/pflichttestgate/gate.go | 1 - ai/internal/pflichttestgate/gate_test.go | 10 +- ai/internal/qa04gate/qa04gate.go | 1 - ai/internal/qa05gate/qa05gate_test.go | 2 +- ai/internal/queue/queue_loadtest_test.go | 2 +- ai/internal/queue/worker_test.go | 6 +- ai/internal/residencyapi/residencyapi.go | 2 +- .../routingsuggestapi/routingsuggestapi.go | 2 +- .../semanticsearchapi/semanticsearchapi.go | 2 +- ai/internal/summaryapi/summaryapi.go | 2 +- ai/internal/summaryquality/summaryquality.go | 6 +- ai/internal/svcauth/svcauth.go | 20 --- ai/internal/tagsuggest/tagsuggest_test.go | 1 - .../tenantregistryclient.go | 47 ------ .../tenantregistryclient_test.go | 59 -------- .../threadsummarize/threadsummarize.go | 6 +- 35 files changed, 38 insertions(+), 474 deletions(-) delete mode 100644 ai/internal/auditclient/auditclient.go delete mode 100644 ai/internal/auditclient/auditclient_test.go delete mode 100644 ai/internal/httpclient/httpclient.go delete mode 100644 ai/internal/httpclient/httpclient_test.go delete mode 100644 ai/internal/loadtest/loadtest.go delete mode 100644 ai/internal/svcauth/svcauth.go delete mode 100644 ai/internal/tenantregistryclient/tenantregistryclient.go delete mode 100644 ai/internal/tenantregistryclient/tenantregistryclient_test.go diff --git a/ai/cmd/ai-app/main.go b/ai/cmd/ai-app/main.go index 64f5321f..56b97d4f 100644 --- a/ai/cmd/ai-app/main.go +++ b/ai/cmd/ai-app/main.go @@ -41,9 +41,9 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearchapi" "gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize" "gitea.perlbach24.de/scripte/nexarch/ai/internal/summaryapi" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantregistryclient" "gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize" "gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex" + "gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient" ) const workerPollInterval = 2 * time.Second diff --git a/ai/cmd/searchchatapi-a11y-fixture/main.go b/ai/cmd/searchchatapi-a11y-fixture/main.go index 3d309a04..10ab2f5e 100644 --- a/ai/cmd/searchchatapi-a11y-fixture/main.go +++ b/ai/cmd/searchchatapi-a11y-fixture/main.go @@ -19,7 +19,7 @@ import ( ) const ( - fixtureTenant = "beispiel-mandant" + fixtureTenant = "beispiel-mandant" fixtureSourceType = "dms_document" fixtureSourceID = "beispiel-dokument-1" ) diff --git a/ai/go.mod b/ai/go.mod index 64a93ad3..184e478e 100644 --- a/ai/go.mod +++ b/ai/go.mod @@ -4,7 +4,10 @@ go 1.25.0 toolchain go1.25.13 -require github.com/jackc/pgx/v5 v5.11.0 +require ( + gitea.perlbach24.de/scripte/nexarch/common v0.1.0 + github.com/jackc/pgx/v5 v5.11.0 +) require ( github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/ai/go.sum b/ai/go.sum index 3f716dd1..ef9d0e21 100644 --- a/ai/go.sum +++ b/ai/go.sum @@ -1,3 +1,5 @@ +gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY= +gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/ai/internal/auditclient/auditclient.go b/ai/internal/auditclient/auditclient.go deleted file mode 100644 index 74f306b3..00000000 --- a/ai/internal/auditclient/auditclient.go +++ /dev/null @@ -1,72 +0,0 @@ -// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08 -// (internal/audit, POST /audit/ingest). AI ist ein physisch getrenntes -// Go-Modul und kann Cores internal/audit daher nicht direkt importieren - -// nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie -// connect/internal/auditclient (ENG-06) und archive/internal/auditclient. -package auditclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - - "gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient" -) - -type Client struct { - BaseURL string - ServiceToken string - HTTPClient *http.Client -} - -func New(baseURL, serviceToken string) *Client { - return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()} -} - -type ingestRequest struct { - TenantSlug string `json:"tenant_slug"` - Actor string `json:"actor"` - Action string `json:"action"` - Target string `json:"target"` - Metadata map[string]any `json:"metadata"` -} - -// Ingest meldet EIN Audit-Ereignis an Core. Absichtlich keine -// Update-/Delete-Methode: der Ingest-Endpunkt (AUD-08) ruft denselben -// alleinigen, anfuegenden Schreibpfad wie core-interne Aufrufer auf -// (audit.Log.Record), gegen den Core selbst per Datenbank-Trigger jede -// nachtraegliche Aenderung/Loeschung verweigert (AUD-02). -func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error { - body, err := json.Marshal(ingestRequest{ - TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata, - }) - if err != nil { - return fmt.Errorf("auditclient: request kodieren: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body)) - if err != nil { - return fmt.Errorf("auditclient: request bauen: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-Service-Token", c.ServiceToken) - - resp, err := c.httpClient().Do(req) - if err != nil { - return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode != http.StatusAccepted { - return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode) - } - return nil -} - -func (c *Client) httpClient() *http.Client { - if c.HTTPClient != nil { - return c.HTTPClient - } - return httpclient.Default() -} diff --git a/ai/internal/auditclient/auditclient_test.go b/ai/internal/auditclient/auditclient_test.go deleted file mode 100644 index bd256b57..00000000 --- a/ai/internal/auditclient/auditclient_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package auditclient - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" -) - -func TestIngest_SendsExpectedRequest(t *testing.T) { - var gotToken string - var gotBody ingestRequest - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotToken = r.Header.Get("X-Service-Token") - _ = json.NewDecoder(r.Body).Decode(&gotBody) - w.WriteHeader(http.StatusAccepted) - })) - defer srv.Close() - - c := New(srv.URL, "test-token") - err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1", - map[string]any{"model": "llama3", "prompt_version": 2}) - if err != nil { - t.Fatalf("Ingest: %v", err) - } - if gotToken != "test-token" { - t.Errorf("token = %q, want test-token", gotToken) - } - if gotBody.Action != "ai.decision.classify" || gotBody.Target != "doc:1" { - t.Errorf("unerwarteter request: %+v", gotBody) - } - if gotBody.Metadata["model"] != "llama3" { - t.Errorf("metadata.model = %v, want llama3", gotBody.Metadata["model"]) - } -} - -func TestIngest_UnexpectedStatusIsError(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "zugriff verweigert", http.StatusForbidden) - })) - defer srv.Close() - - c := New(srv.URL, "test-token") - if err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1", nil); err == nil { - t.Fatal("erwartet fehler bei status 403, habe nil") - } -} diff --git a/ai/internal/classify/classify_test.go b/ai/internal/classify/classify_test.go index ae3d0e4d..a812b75c 100644 --- a/ai/internal/classify/classify_test.go +++ b/ai/internal/classify/classify_test.go @@ -9,11 +9,11 @@ import ( "strings" "testing" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient" "gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama" + "gitea.perlbach24.de/scripte/nexarch/common/auditclient" ) var testSchema = Schema{Categories: []string{"rechnung", "vertrag", "kuendigung"}} diff --git a/ai/internal/decisionaudit/decisionaudit.go b/ai/internal/decisionaudit/decisionaudit.go index b7aeb525..3379a217 100644 --- a/ai/internal/decisionaudit/decisionaudit.go +++ b/ai/internal/decisionaudit/decisionaudit.go @@ -8,7 +8,7 @@ import ( "context" "fmt" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient" + "gitea.perlbach24.de/scripte/nexarch/common/auditclient" ) // Decision ist eine einzelne, abgeschlossene KI-gestuetzte Entscheidung diff --git a/ai/internal/decisionaudit/decisionaudit_test.go b/ai/internal/decisionaudit/decisionaudit_test.go index 4c2199f9..ab713fc1 100644 --- a/ai/internal/decisionaudit/decisionaudit_test.go +++ b/ai/internal/decisionaudit/decisionaudit_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient" + "gitea.perlbach24.de/scripte/nexarch/common/auditclient" ) type recordedRequest struct { diff --git a/ai/internal/docchat/docchat_qa08_loadtest_test.go b/ai/internal/docchat/docchat_qa08_loadtest_test.go index 7e40f221..5ca5124a 100644 --- a/ai/internal/docchat/docchat_qa08_loadtest_test.go +++ b/ai/internal/docchat/docchat_qa08_loadtest_test.go @@ -7,10 +7,10 @@ import ( "testing" "time" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama" "gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch" + "gitea.perlbach24.de/scripte/nexarch/common/loadtest" ) // QA-08 Pruefung 1 (AK1): echte parallele Chat-Anfragen gegen den realen, diff --git a/ai/internal/docchatapi/docchatapi.go b/ai/internal/docchatapi/docchatapi.go index fd7fda38..2b5d48e5 100644 --- a/ai/internal/docchatapi/docchatapi.go +++ b/ai/internal/docchatapi/docchatapi.go @@ -12,8 +12,8 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/docchat" "gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" "gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/extractapi/extractapi.go b/ai/internal/extractapi/extractapi.go index 97fe4a39..340065ee 100644 --- a/ai/internal/extractapi/extractapi.go +++ b/ai/internal/extractapi/extractapi.go @@ -15,7 +15,7 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/extractvalidate" "gitea.perlbach24.de/scripte/nexarch/ai/internal/invoiceextract" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/extractconfirmapi/extractconfirmapi.go b/ai/internal/extractconfirmapi/extractconfirmapi.go index cecf67bb..55937bb7 100644 --- a/ai/internal/extractconfirmapi/extractconfirmapi.go +++ b/ai/internal/extractconfirmapi/extractconfirmapi.go @@ -15,7 +15,7 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping" "gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/httpclient/httpclient.go b/ai/internal/httpclient/httpclient.go deleted file mode 100644 index cf7aaaf8..00000000 --- a/ai/internal/httpclient/httpclient.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe -// (Kopie des Musters aus connect/internal/httpclient, ENG-06): Zeitlimit -// statt endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde -// sonst an das Weiterleitungsziel mitgesendet). -package httpclient - -import ( - "net/http" - "time" -) - -const DefaultTimeout = 15 * time.Second - -var shared = New(DefaultTimeout) - -func Default() *http.Client { return shared } - -func New(timeout time.Duration) *http.Client { - return &http.Client{ - Timeout: timeout, - CheckRedirect: func(*http.Request, []*http.Request) error { - return http.ErrUseLastResponse - }, - } -} diff --git a/ai/internal/httpclient/httpclient_test.go b/ai/internal/httpclient/httpclient_test.go deleted file mode 100644 index 5d8d3eeb..00000000 --- a/ai/internal/httpclient/httpclient_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package httpclient - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestNew_DoesNotFollowRedirects(t *testing.T) { - target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("weiterleitungsziel haette nie erreicht werden duerfen") - })) - defer target.Close() - - redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, target.URL, http.StatusFound) - })) - defer redirector.Close() - - resp, err := Default().Get(redirector.URL) - if err != nil { - t.Fatalf("Get: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusFound { - t.Fatalf("status = %d, want %d (keine weiterleitung gefolgt)", resp.StatusCode, http.StatusFound) - } -} diff --git a/ai/internal/loadtest/loadtest.go b/ai/internal/loadtest/loadtest.go deleted file mode 100644 index 51dc5237..00000000 --- a/ai/internal/loadtest/loadtest.go +++ /dev/null @@ -1,138 +0,0 @@ -// Package loadtest implementiert die fuer QA-07/QA-08 gemeinsam genutzte -// Last- und Leistungsmessung: parallele Sessions ausfuehren, Latenzen -// aufzeichnen, Durchsatz bestimmen. k6/vegeta sind auf diesem Rechner nicht -// installierbar (Projektregel: keine zusaetzlichen Toolchains/Dienste -// installieren) — echter Ersatz ohne externe Abhaengigkeit: reale -// nebenlaeufige Aufrufe ueber die Go-Standardbibliothek, gegen den echten, -// in dieser Sitzung gebauten Code. Eigenstaendige Kopie aus -// mail/internal/loadtest, da Go-Module keine internal/-Pakete teilen. -package loadtest - -import ( - "context" - "os" - "runtime" - "sort" - "strconv" - "strings" - "sync" - "time" -) - -// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs zusammen. -type Result struct { - Protocol string - Concurrency int - Sessions int - Errors int - Duration time.Duration - Latencies []time.Duration // aufsteigend sortiert -} - -// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit. -func (r Result) Throughput() float64 { - if r.Duration <= 0 { - return 0 - } - return float64(r.Sessions) / r.Duration.Seconds() -} - -// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0). -func (r Result) Percentile(p float64) time.Duration { - if len(r.Latencies) == 0 { - return 0 - } - idx := int(p * float64(len(r.Latencies)-1)) - if idx < 0 { - idx = 0 - } - if idx >= len(r.Latencies) { - idx = len(r.Latencies) - 1 - } - return r.Latencies[idx] -} - -// Max liefert die höchste gemessene Latenz. -func (r Result) Max() time.Duration { - if len(r.Latencies) == 0 { - return 0 - } - return r.Latencies[len(r.Latencies)-1] -} - -// Run führt totalSessions Sessions über maximal concurrency parallele -// Worker aus und misst je Session die Latenz von sessionFunc. -func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result { - var mu sync.Mutex - latencies := make([]time.Duration, 0, totalSessions) - errCount := 0 - - sem := make(chan struct{}, concurrency) - var wg sync.WaitGroup - start := time.Now() - for i := 0; i < totalSessions; i++ { - sem <- struct{}{} - wg.Add(1) - go func() { - defer wg.Done() - defer func() { <-sem }() - t0 := time.Now() - err := sessionFunc(ctx) - elapsed := time.Since(t0) - mu.Lock() - latencies = append(latencies, elapsed) - if err != nil { - errCount++ - } - mu.Unlock() - }() - } - wg.Wait() - duration := time.Since(start) - - sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) - return Result{ - Protocol: protocol, - Concurrency: concurrency, - Sessions: totalSessions, - Errors: errCount, - Duration: duration, - Latencies: latencies, - } -} - -// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs DIESES -// Prozesses: Heap über die Go-Runtime, kumulierte CPU-Zeit über -// /proc/self/stat (Linux, kein externes Werkzeug nötig). -type ResourceSample struct { - HeapAllocBytes uint64 - CPUTimeSeconds float64 -} - -func SampleResources() ResourceSample { - var m runtime.MemStats - runtime.ReadMemStats(&m) - return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()} -} - -// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit dieses -// Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht lesbar ist -// (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das als "keine -// CPU-Messung verfügbar", kein harter Fehler. -func readSelfCPUTimeSeconds() float64 { - data, err := os.ReadFile("/proc/self/stat") - if err != nil { - return 0 - } - fields := strings.Fields(string(data)) - if len(fields) < 15 { - return 0 - } - utime, err1 := strconv.ParseFloat(fields[13], 64) - stime, err2 := strconv.ParseFloat(fields[14], 64) - if err1 != nil || err2 != nil { - return 0 - } - const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ - return (utime + stime) / clockTicksPerSecond -} diff --git a/ai/internal/metadatapushclient/metadatapushclient.go b/ai/internal/metadatapushclient/metadatapushclient.go index ebb59afd..961e72fc 100644 --- a/ai/internal/metadatapushclient/metadatapushclient.go +++ b/ai/internal/metadatapushclient/metadatapushclient.go @@ -15,7 +15,7 @@ import ( "net/url" "sort" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient" + "gitea.perlbach24.de/scripte/nexarch/common/httpclient" ) type Client struct { diff --git a/ai/internal/metadatapushclient/metadatapushclient_test.go b/ai/internal/metadatapushclient/metadatapushclient_test.go index 60eec1f9..66b87a48 100644 --- a/ai/internal/metadatapushclient/metadatapushclient_test.go +++ b/ai/internal/metadatapushclient/metadatapushclient_test.go @@ -81,7 +81,7 @@ func TestPush_UnknownFieldKey_ReportedNotDropped(t *testing.T) { c := New(srv.URL, "svc-token-123") result, err := c.Push(context.Background(), "doc-1", "ai", map[string]string{ - "rechnungsdatum": "2026-01-15", + "rechnungsdatum": "2026-01-15", "nicht_existent_feld": "wert", }) if err != nil { diff --git a/ai/internal/moduletypeapi/moduletypeapi.go b/ai/internal/moduletypeapi/moduletypeapi.go index 687254e5..e21d7d76 100644 --- a/ai/internal/moduletypeapi/moduletypeapi.go +++ b/ai/internal/moduletypeapi/moduletypeapi.go @@ -10,7 +10,7 @@ import ( "net/http" "gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/pflichttestgate/gate.go b/ai/internal/pflichttestgate/gate.go index abf2265a..6bae7d47 100644 --- a/ai/internal/pflichttestgate/gate.go +++ b/ai/internal/pflichttestgate/gate.go @@ -17,7 +17,6 @@ import ( // 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/`), diff --git a/ai/internal/pflichttestgate/gate_test.go b/ai/internal/pflichttestgate/gate_test.go index 022d615e..80fb383a 100644 --- a/ai/internal/pflichttestgate/gate_test.go +++ b/ai/internal/pflichttestgate/gate_test.go @@ -1,24 +1,24 @@ // Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit -// geänderter ai/internal/svcauth/svcauth.go ohne begleitende Testdatei +// geänderter ai/internal/tenantfeature/tenantfeature.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"}) + violations := CheckDiff([]string{"ai/internal/tenantfeature/tenantfeature.go"}) if len(violations) != 1 { t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations) } - if violations[0].Package != "ai/internal/svcauth" { + if violations[0].Package != "ai/internal/tenantfeature" { 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", + "ai/internal/tenantfeature/tenantfeature.go", + "ai/internal/tenantfeature/tenantfeature_test.go", }) if len(violations) != 0 { t.Fatalf("erwartet keine verstöße, habe: %+v", violations) diff --git a/ai/internal/qa04gate/qa04gate.go b/ai/internal/qa04gate/qa04gate.go index 7f2983b4..e43810c2 100644 --- a/ai/internal/qa04gate/qa04gate.go +++ b/ai/internal/qa04gate/qa04gate.go @@ -231,4 +231,3 @@ func (p *embedProvider) Chat(ctx context.Context, req provider.ChatRequest) (pro func (p *embedProvider) Complete(ctx context.Context, req provider.CompletionRequest) (provider.CompletionResponse, error) { return provider.CompletionResponse{}, errors.New("qa04gate: Complete wird vom Prüfgate nicht genutzt") } - diff --git a/ai/internal/qa05gate/qa05gate_test.go b/ai/internal/qa05gate/qa05gate_test.go index 586cb174..a1334331 100644 --- a/ai/internal/qa05gate/qa05gate_test.go +++ b/ai/internal/qa05gate/qa05gate_test.go @@ -16,7 +16,6 @@ import ( "github.com/jackc/pgx/v5/pgxpool" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient" "gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit" "gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider" @@ -25,6 +24,7 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/residency" "gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantfeature" "gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb" + "gitea.perlbach24.de/scripte/nexarch/common/auditclient" ) // realPool liefert einen frischen, migrierten Pool auf der Wegwerf-DB aus diff --git a/ai/internal/queue/queue_loadtest_test.go b/ai/internal/queue/queue_loadtest_test.go index 5f664bab..43024d88 100644 --- a/ai/internal/queue/queue_loadtest_test.go +++ b/ai/internal/queue/queue_loadtest_test.go @@ -8,8 +8,8 @@ import ( "time" "gitea.perlbach24.de/scripte/nexarch/ai/internal/limits" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest" "gitea.perlbach24.de/scripte/nexarch/ai/internal/provider" + "gitea.perlbach24.de/scripte/nexarch/common/loadtest" ) // QA-08 Pruefung 2 (AK2): realistische parallele Klassifizierungs-/ diff --git a/ai/internal/queue/worker_test.go b/ai/internal/queue/worker_test.go index 3e2ce900..4a960d66 100644 --- a/ai/internal/queue/worker_test.go +++ b/ai/internal/queue/worker_test.go @@ -16,9 +16,9 @@ import ( // eine Zeitueberschreitung real simulieren laesst (respektiert ctx wie ein // echter HTTP-Client es tun wuerde). type slowProvider struct { - delay time.Duration - calls atomic.Int64 - cancelled atomic.Int64 + delay time.Duration + calls atomic.Int64 + cancelled atomic.Int64 } func (s *slowProvider) Chat(ctx context.Context, req provider.ChatRequest) (provider.ChatResponse, error) { diff --git a/ai/internal/residencyapi/residencyapi.go b/ai/internal/residencyapi/residencyapi.go index fb7237f0..1faf9247 100644 --- a/ai/internal/residencyapi/residencyapi.go +++ b/ai/internal/residencyapi/residencyapi.go @@ -9,7 +9,7 @@ import ( "net/http" "gitea.perlbach24.de/scripte/nexarch/ai/internal/residency" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/routingsuggestapi/routingsuggestapi.go b/ai/internal/routingsuggestapi/routingsuggestapi.go index 715326a6..7913c15a 100644 --- a/ai/internal/routingsuggestapi/routingsuggestapi.go +++ b/ai/internal/routingsuggestapi/routingsuggestapi.go @@ -13,7 +13,7 @@ import ( "gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest" "gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/semanticsearchapi/semanticsearchapi.go b/ai/internal/semanticsearchapi/semanticsearchapi.go index f7109630..7d8db564 100644 --- a/ai/internal/semanticsearchapi/semanticsearchapi.go +++ b/ai/internal/semanticsearchapi/semanticsearchapi.go @@ -10,7 +10,7 @@ import ( "net/http" "gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/summaryapi/summaryapi.go b/ai/internal/summaryapi/summaryapi.go index 0928ffd4..7ed052af 100644 --- a/ai/internal/summaryapi/summaryapi.go +++ b/ai/internal/summaryapi/summaryapi.go @@ -10,8 +10,8 @@ import ( "net/http" "gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize" - "gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth" "gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize" + "gitea.perlbach24.de/scripte/nexarch/common/svcauth" ) type Server struct { diff --git a/ai/internal/summaryquality/summaryquality.go b/ai/internal/summaryquality/summaryquality.go index 1ec9bc50..7d5bd8ab 100644 --- a/ai/internal/summaryquality/summaryquality.go +++ b/ai/internal/summaryquality/summaryquality.go @@ -125,10 +125,10 @@ func wordSet(s string) map[string]bool { // TypeMetrics fasst die Bewertungen aller Stichproben eines // Zusammenfassungstyps zusammen (AK1). type TypeMetrics struct { - Type string - Count int + Type string + Count int AverageGroundedness float64 - FlaggedCount int + FlaggedCount int } // AggregateByType berechnet je Zusammenfassungstyp die Kennzahl über alle diff --git a/ai/internal/svcauth/svcauth.go b/ai/internal/svcauth/svcauth.go deleted file mode 100644 index 5c065de3..00000000 --- a/ai/internal/svcauth/svcauth.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package svcauth vergleicht Service-Token in konstanter Zeit (eigene Kopie -// je Modul, siehe workflow/internal/svcauth — Go-Module teilen keine -// internal/-Pakete über Modulgrenzen; Lehre aus RET-14/F-08: Schutz beim -// ersten Entwurf einbauen statt nachzuruesten). -package svcauth - -import ( - "crypto/sha256" - "crypto/subtle" -) - -// Equal vergleicht die SHA-256-Werte beider Eingaben in konstanter Zeit. -// Ein leeres erwartetes Token stimmt nie. -func Equal(got, want string) bool { - if want == "" { - return false - } - g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want)) - return subtle.ConstantTimeCompare(g[:], w[:]) == 1 -} diff --git a/ai/internal/tagsuggest/tagsuggest_test.go b/ai/internal/tagsuggest/tagsuggest_test.go index 930b806d..ac4242f0 100644 --- a/ai/internal/tagsuggest/tagsuggest_test.go +++ b/ai/internal/tagsuggest/tagsuggest_test.go @@ -27,7 +27,6 @@ func (s *stubProvider) Embed(ctx context.Context, req provider.EmbeddingRequest) return provider.EmbeddingResponse{}, nil } - // AK1: Konfidenz wird je Vorschlag mitgeliefert. func TestSuggest_ReturnsConfidencePerSuggestion(t *testing.T) { p := &stubProvider{response: `[{"tag":"rechnung","confidence":0.9,"reasoning":"enthaelt rechnungsnummer"},{"tag":"steuer","confidence":0.4,"reasoning":"erwaehnt umsatzsteuer"}]`} diff --git a/ai/internal/tenantregistryclient/tenantregistryclient.go b/ai/internal/tenantregistryclient/tenantregistryclient.go deleted file mode 100644 index e768c264..00000000 --- a/ai/internal/tenantregistryclient/tenantregistryclient.go +++ /dev/null @@ -1,47 +0,0 @@ -// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core -// TEN-01 (Tenant-Registry); AI ist ein eigenes Go-Modul und kann Cores -// Pakete nicht importieren. Gleiches Muster wie archive/internal/ -// tenantregistryclient und dms/internal/tenantregistryclient (RET-12). -package tenantregistryclient - -import ( - "context" - "fmt" - "net/http" - "net/url" - "time" -) - -type Client struct { - BaseURL string - ServiceToken string - HTTPClient *http.Client -} - -func New(baseURL, serviceToken string) *Client { - return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: &http.Client{Timeout: 10 * time.Second}} -} - -// VerifyTenant fragt Core, ob tenantSlug als Tenant gefuehrt wird. Transport- -// und Serverfehler kommen als Fehler zurueck, nie als "gueltig": der Aufrufer -// muss fail-closed reagieren. -func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil) - if err != nil { - return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err) - } - req.Header.Set("X-Service-Token", c.ServiceToken) - resp, err := c.HTTPClient.Do(req) - if err != nil { - return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err) - } - defer func() { _ = resp.Body.Close() }() - switch resp.StatusCode { - case http.StatusOK: - return true, nil - case http.StatusNotFound: - return false, nil - default: - return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode) - } -} diff --git a/ai/internal/tenantregistryclient/tenantregistryclient_test.go b/ai/internal/tenantregistryclient/tenantregistryclient_test.go deleted file mode 100644 index 41f9f355..00000000 --- a/ai/internal/tenantregistryclient/tenantregistryclient_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package tenantregistryclient - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" -) - -func TestVerifyTenant_ExistingTenant(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/tenants/mandant-a" { - t.Errorf("pfad = %q, want /tenants/mandant-a", r.URL.Path) - } - w.WriteHeader(http.StatusOK) - })) - defer server.Close() - - c := New(server.URL, "test-token") - ok, err := c.VerifyTenant(context.Background(), "mandant-a") - if err != nil { - t.Fatalf("verifytenant: %v", err) - } - if !ok { - t.Fatal("erwartet gueltigen tenant, habe false") - } -} - -func TestVerifyTenant_UnknownTenant(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - defer server.Close() - - c := New(server.URL, "test-token") - ok, err := c.VerifyTenant(context.Background(), "unbekannt") - if err != nil { - t.Fatalf("verifytenant: %v", err) - } - if ok { - t.Fatal("erwartet ungueltigen tenant, habe true") - } -} - -func TestVerifyTenant_ServerErrorIsFailClosed(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - - c := New(server.URL, "test-token") - ok, err := c.VerifyTenant(context.Background(), "mandant-a") - if err == nil { - t.Fatal("erwartet fehler bei 500-antwort der tenant-registry") - } - if ok { - t.Fatal("erwartet false bei fehler (fail-closed)") - } -} diff --git a/ai/internal/threadsummarize/threadsummarize.go b/ai/internal/threadsummarize/threadsummarize.go index e5c29d61..cac4a083 100644 --- a/ai/internal/threadsummarize/threadsummarize.go +++ b/ai/internal/threadsummarize/threadsummarize.go @@ -30,9 +30,9 @@ type Thread struct { // Result ist die Zusammenfassung eines Threads. type Result struct { - Summary string - MessageCount int - LastMessageID string + Summary string + MessageCount int + LastMessageID string } // Options steuert die Zusammenfassung. -- 2.47.3