-
-
Notifications
You must be signed in to change notification settings - Fork 74
feat(audit): record ingress rewriters that changed nothing #594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -437,6 +437,81 @@ func TestRequestRewriteMiddlewareRecordsRevisions(t *testing.T) { | |
| }) | ||
| } | ||
|
|
||
| func TestRequestRewriteMiddlewareRecordsNoChangeRevisions(t *testing.T) { | ||
| // A rewriter that inspects the request and forwards it untouched is | ||
| // still a step operators need to see, so it gets a no-change revision. | ||
| quiet := &stubRewriter{name: "quiet"} | ||
| // Response headers and a structured detail without a body change (a | ||
| // rewriter annotating why it did nothing) must not turn the step into a | ||
| // real revision — but the detail is the explanation, so it is kept. | ||
| annotating := &stubRewriter{ | ||
| name: "annotating", | ||
| rewrite: func(ext.Input) (*ext.Result, error) { | ||
| header := http.Header{} | ||
| header.Set("X-Test-Rewriter", "skipped") | ||
| return &ext.Result{ | ||
| ResponseHeader: header, | ||
| Detail: map[string]any{"reason": "nothing to compress"}, | ||
| }, nil | ||
| }, | ||
| } | ||
|
|
||
| auditLogger := &capturingAuditLogger{config: auditlog.Config{Enabled: true, LogBodies: true}} | ||
| srv := New(newRewriteTestProvider(), &Config{ | ||
| AuditLogger: auditLogger, | ||
| RequestRewriters: []ext.RequestRewriter{ | ||
| quiet, | ||
| replaceBodyRewriter("swap", "PING", "PONG"), | ||
| annotating, | ||
| }, | ||
| }) | ||
| rec := postJSON(t, srv, "/v1/chat/completions", | ||
| `{"model":"gpt-4o-mini","messages":[{"role":"user","content":"PING"}]}`) | ||
| if rec.Code != http.StatusOK { | ||
| t.Fatalf("expected 200, got %d (%s)", rec.Code, rec.Body.String()) | ||
| } | ||
| if rec.Header().Get("X-Test-Rewriter") != "skipped" { | ||
| t.Error("response headers from a no-change rewriter must still be applied") | ||
| } | ||
| if len(auditLogger.entries) == 0 { | ||
| t.Fatal("expected an audit entry") | ||
| } | ||
|
|
||
| revisions := auditLogger.entries[0].Data.RequestRevisions | ||
| if len(revisions) != 3 { | ||
| t.Fatalf("expected 3 revisions (2 no-change + 1 rewrite), got %d: %+v", len(revisions), revisions) | ||
| } | ||
| for i, want := range []struct { | ||
| rewriter string | ||
| noChange bool | ||
| }{{"quiet", true}, {"swap", false}, {"annotating", true}} { | ||
| got := revisions[i] | ||
| if got.Seq != i+1 || got.Rewriter != want.rewriter || got.NoChange != want.noChange { | ||
| t.Errorf("revision %d = %+v, want rewriter %q no_change=%v", i+1, got, want.rewriter, want.noChange) | ||
| } | ||
| } | ||
|
|
||
| quietRev := revisions[0] | ||
| if quietRev.BytesBefore == 0 || quietRev.BytesAfter != quietRev.BytesBefore { | ||
| t.Errorf("no-change revision must report equal sizes: %+v", quietRev) | ||
| } | ||
| if quietRev.Body != nil || quietRev.TokensSaved != 0 { | ||
| t.Errorf("no-change revision must carry no body or savings: %+v", quietRev) | ||
| } | ||
| // The trailing no-change step sees the body the previous rewriter produced. | ||
| if revisions[2].BytesBefore != revisions[1].BytesAfter { | ||
| t.Errorf("no-change revision must measure the current body: %+v", revisions[2]) | ||
| } | ||
| // A rewriter that reports why it changed nothing keeps that explanation. | ||
| detail, ok := revisions[2].Detail.(map[string]any) | ||
| if !ok || detail["reason"] != "nothing to compress" { | ||
| t.Errorf("no-change revision must keep the rewriter detail, got %+v", revisions[2].Detail) | ||
| } | ||
| if quietRev.Detail != nil { | ||
| t.Errorf("a rewriter that returned no result has no detail to record: %+v", quietRev) | ||
| } | ||
| } | ||
|
Comment on lines
+440
to
+513
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Make the new behavior coverage table-driven. Model the nil-result, header-only/nil-body, and body-rewrite cases as table entries/subtests. As per coding guidelines, “Add or update table-driven tests for behavior changes.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| func TestRequestRewriteMiddlewareStoresTokensSavedInContext(t *testing.T) { | ||
| compressor := &stubRewriter{ | ||
| name: "compressor", | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a rewriter returns a nil body with structured
Detail, this branch records an unchanged revision without passing that detail, preventing the audit trail from explaining why the rewriter made no change.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!