Files
llmgw/internal/audit/audit_test.go
T
Ilker ManapandClaude Opus 5.5 249a91aeec Guardrails turn off and on; a judge's refusal says which hazard; ctl guard judge ask
guard.enabled (unset is on) makes every policy check nothing while false,
live: llmgw ctl guard off/on set it in the cluster's settings, and ctl
status, ctl guard list and the guardrails page say when they are off.

A refusal by a judge names the hazard it saw - judge: S6,Specialized
Advice - and the verdict is kept with the request in a new usage column,
guard_rule, shown by ctl request, the request log and the guardrails page.

llmgw ctl guard judge ask has a judge read a text, past the cache, and shows
its raw answer and how it is read; the endpoint is kept out of the audit so
no text is recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 12:41:08 +02:00

85 lines
2.8 KiB
Go

package audit
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
func TestNothingSecretIsRecorded(t *testing.T) {
got := Redact(FormFields(url.Values{
"current": {"old-pw"}, "new": {"new-pw"}, "password": {"pw"}, "csrf": {"tok"},
"name": {"ok-name"}, "note": {"sk-proj-abcdefgh"},
}))
for _, secret := range []string{"old-pw", "new-pw", `"pw"`, "tok", "sk-proj"} {
if strings.Contains(got, secret) {
t.Errorf("%s recorded: %s", secret, got)
}
}
if !strings.Contains(got, "ok-name") {
t.Errorf("an ordinary field was dropped: %s", got)
}
nested := Redact(map[string]any{"external": map[string]any{"api_key": "x1", "url": "https://p"}})
if strings.Contains(nested, "x1") || !strings.Contains(nested, "https://p") {
t.Errorf("nested: %s", nested)
}
}
type memSink struct{ got []store.AuditEvent }
func (m *memSink) WriteAudit(_ context.Context, e store.AuditEvent) error {
m.got = append(m.got, e)
return nil
}
func TestEveryChangeToTheAdminAPIIsRecorded(t *testing.T) {
sink := &memSink{}
h := API(sink, func(*http.Request) string { return "ilker" }, nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
}))
for _, req := range []*http.Request{
httptest.NewRequest("GET", "/api/v1/models", nil),
httptest.NewRequest("POST", "/api/v1/models", strings.NewReader(`{"name":"m1","external":{"api_key":"sk-1"}}`)),
httptest.NewRequest("DELETE", "/api/v1/nodes/n1", nil),
} {
h.ServeHTTP(httptest.NewRecorder(), req)
}
if len(sink.got) != 2 {
t.Fatalf("recorded %d: %+v", len(sink.got), sink.got)
}
e := sink.got[0]
if e.Actor != "ilker" || e.Action != "POST /api/v1/models" || e.Target != "m1" || e.Status != 201 ||
strings.Contains(e.Detail, "sk-1") || e.Source != "api" {
t.Errorf("%+v", e)
}
}
func TestABodyLargerThanWhatIsRecordedReachesTheHandlerWhole(t *testing.T) {
big := `{"content":"` + strings.Repeat("x", 200<<10) + `"}`
var got int
h := API(&memSink{}, func(*http.Request) string { return "ilker" }, nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
got = len(b)
}))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/api/v1/models/m/eval", strings.NewReader(big)))
if got != len(big) {
t.Errorf("the handler was given %d of %d bytes", got, len(big))
}
}
func TestAskingAJudgeKeepsNoText(t *testing.T) {
sink := &memSink{}
h := API(sink, func(*http.Request) string { return "ilker" }, nil, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("POST", "/api/v1/guard/judges/ask",
strings.NewReader(`{"text":"something a user wrote"}`)))
if len(sink.got) != 0 {
t.Errorf("a judge's question was recorded: %+v", sink.got)
}
}