Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
263
backend/internal/signature/gemini_sanitize_test.go
Normal file
263
backend/internal/signature/gemini_sanitize_test.go
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
package signature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func newSignatureDebugHook(t *testing.T) *test.Hook {
|
||||
t.Helper()
|
||||
|
||||
previousLevel := log.GetLevel()
|
||||
log.SetLevel(log.DebugLevel)
|
||||
hook := test.NewLocal(log.StandardLogger())
|
||||
t.Cleanup(func() {
|
||||
hook.Reset()
|
||||
log.SetLevel(previousLevel)
|
||||
})
|
||||
return hook
|
||||
}
|
||||
|
||||
func assertSignatureDebugDoesNotLeak(t *testing.T, hook *test.Hook, forbidden string) {
|
||||
t.Helper()
|
||||
|
||||
if forbidden == "" {
|
||||
return
|
||||
}
|
||||
for _, entry := range hook.AllEntries() {
|
||||
if strings.Contains(entry.Message, forbidden) {
|
||||
t.Fatalf("debug log leaked signature in message: %q", entry.Message)
|
||||
}
|
||||
for key, value := range entry.Data {
|
||||
if strings.Contains(fmt.Sprint(value), forbidden) {
|
||||
t.Fatalf("debug log leaked signature in field %q: %v", key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var benchmarkSanitizeGeminiRequestOutput []byte
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesPreservesGeminiSignature(t *testing.T) {
|
||||
sig := testGemini3ThoughtSignature([]byte{0x01, 0x0c, 0x39})
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","args":{}},"thoughtSignature":"` + sig + `"}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != sig {
|
||||
t.Fatalf("thoughtSignature = %q, want %q. Output: %s", got, sig, string(out))
|
||||
}
|
||||
if &out[0] != &input[0] {
|
||||
t.Fatal("compatible canonical signature payload was copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesNormalizesDuplicateCanonicalField(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","args":{}},"thoughtSignature":"` + GeminiSkipThoughtSignatureValidator + `","thoughtSignature":"bad","thoughtSignature":"worse"}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != GeminiSkipThoughtSignatureValidator {
|
||||
t.Fatalf("thoughtSignature = %q, want bypass sentinel. Output: %s", got, out)
|
||||
}
|
||||
if count := strings.Count(string(out), `"thoughtSignature"`); count != 1 {
|
||||
t.Fatalf("thoughtSignature field count = %d, want 1. Output: %s", count, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesParallelSyntheticOnlyFirstGetsBypass(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"first","args":{}}},{"functionCall":{"name":"second","args":{}}}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != GeminiSkipThoughtSignatureValidator {
|
||||
t.Fatalf("first call signature = %q, want bypass sentinel; output=%s", got, out)
|
||||
}
|
||||
if signature := gjson.GetBytes(out, "contents.0.parts.1.thoughtSignature"); signature.Exists() {
|
||||
t.Fatalf("second parallel call should remain unsigned; output=%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesNativeParallelPreservesUnsignedSibling(t *testing.T) {
|
||||
nativeSignature := testGemini3ThoughtSignature([]byte{0x01, 0x0c, 0x39})
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"first","args":{}},"thoughtSignature":"` + nativeSignature + `"},{"functionCall":{"name":"second","args":{}}}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != nativeSignature {
|
||||
t.Fatalf("first call signature = %q, want native signature; output=%s", got, out)
|
||||
}
|
||||
if signature := gjson.GetBytes(out, "contents.0.parts.1.thoughtSignature"); signature.Exists() {
|
||||
t.Fatalf("native unsigned sibling should remain unsigned; output=%s", out)
|
||||
}
|
||||
if &out[0] != &input[0] {
|
||||
t.Fatal("already-native parallel history was copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesRemovesPollutedSiblingBypass(t *testing.T) {
|
||||
nativeSignature := testGemini3ThoughtSignature([]byte{0x01, 0x0c, 0x39})
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"first","args":{}},"thoughtSignature":"` + nativeSignature + `"},{"functionCall":{"name":"second","args":{}},"thoughtSignature":"` + GeminiSkipThoughtSignatureValidator + `"}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != nativeSignature {
|
||||
t.Fatalf("first call signature = %q, want native signature; output=%s", got, out)
|
||||
}
|
||||
if signature := gjson.GetBytes(out, "contents.0.parts.1.thoughtSignature"); signature.Exists() {
|
||||
t.Fatalf("polluted sibling bypass should be removed; output=%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesRemovesPrefixedSiblingBypass(t *testing.T) {
|
||||
nativeSignature := testGemini3ThoughtSignature([]byte{0x01, 0x0c, 0x39})
|
||||
for _, prefix := range []string{"gemini", "google"} {
|
||||
t.Run(prefix, func(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"first","args":{}},"thoughtSignature":"` + nativeSignature + `"},{"functionCall":{"name":"second","args":{}},"thoughtSignature":"` + prefix + `#` + GeminiSkipThoughtSignatureValidator + `"}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if signature := gjson.GetBytes(out, "contents.0.parts.1.thoughtSignature"); signature.Exists() {
|
||||
t.Fatalf("prefixed sibling bypass should be removed; output=%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesLeavesUnsignedThoughtUnsigned(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"text":"hidden","thought":true}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if signature := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature"); signature.Exists() {
|
||||
t.Fatalf("unsigned thought should remain unsigned; output=%s", out)
|
||||
}
|
||||
if &out[0] != &input[0] {
|
||||
t.Fatal("unsigned thought payload was copied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesReusesUnsignedFunctionResponsePayload(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"user","parts":[{"functionResponse":{"name":"f","response":{"result":"ok"}}}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if &out[0] != &input[0] {
|
||||
t.Fatal("unsigned function response payload was copied")
|
||||
}
|
||||
if string(out) != string(input) {
|
||||
t.Fatalf("payload changed:\n got: %s\nwant: %s", out, input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesReplacesBase64UUIDFunctionCall(t *testing.T) {
|
||||
sig := testGeminiThoughtSignature([]byte("e24830a7-5cd6-42fe-998b-ee539e72b9c3"))
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","args":{},"thoughtSignature":"` + sig + `"}}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != GeminiSkipThoughtSignatureValidator {
|
||||
t.Fatalf("thoughtSignature = %q, want bypass sentinel. Output: %s", got, string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "contents.0.parts.0.functionCall.thoughtSignature").Exists() {
|
||||
t.Fatalf("nested functionCall thoughtSignature should be removed. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesLogsBypassReplacement(t *testing.T) {
|
||||
hook := newSignatureDebugHook(t)
|
||||
sig := testGeminiThoughtSignature([]byte("e24830a7-5cd6-42fe-998b-ee539e72b9c3"))
|
||||
input := []byte(`{"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","args":{},"thoughtSignature":"` + sig + `"}}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
if got := gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").String(); got != GeminiSkipThoughtSignatureValidator {
|
||||
t.Fatalf("thoughtSignature = %q, want bypass sentinel. Output: %s", got, string(out))
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, entry := range hook.AllEntries() {
|
||||
if entry.Level != log.DebugLevel {
|
||||
continue
|
||||
}
|
||||
if entry.Data["component"] != "signature_sanitizer" ||
|
||||
entry.Data["target_provider"] != string(SignatureProviderGemini) ||
|
||||
entry.Data["action"] != "replace_with_gemini_bypass" {
|
||||
continue
|
||||
}
|
||||
if entry.Data["block_kind"] != string(SignatureBlockKindGeminiFunctionCall) {
|
||||
t.Fatalf("block_kind = %v, want %s", entry.Data["block_kind"], SignatureBlockKindGeminiFunctionCall)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("expected debug log for Gemini thoughtSignature bypass replacement")
|
||||
}
|
||||
assertSignatureDebugDoesNotLeak(t, hook, sig)
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesPreservesField2WrappedUUIDFunctionCall(t *testing.T) {
|
||||
sig := testGemini3ThoughtSignature([]byte("e24830a7-5cd6-42fe-998b-ee539e72b9c3"))
|
||||
input := []byte(`{"request":{"contents":[{"role":"model","parts":[{"functionCall":{"name":"f","args":{}},"thoughtSignature":"` + sig + `"}]}]}}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "request.contents")
|
||||
|
||||
if got := gjson.GetBytes(out, "request.contents.0.parts.0.thoughtSignature").String(); got != sig {
|
||||
t.Fatalf("thoughtSignature = %q, want wrapped UUID signature preserved. Output: %s", got, string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitizeGeminiRequestThoughtSignaturesNormalizedHistory(b *testing.B) {
|
||||
for _, turns := range []int{1, 16, 64} {
|
||||
b.Run(fmt.Sprintf("turns_%d", turns), func(b *testing.B) {
|
||||
input := normalizedGeminiSignatureHistory(turns, 8<<20)
|
||||
output := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
if &output[0] != &input[0] {
|
||||
b.Fatal("normalized payload was copied")
|
||||
}
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(input)))
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
benchmarkSanitizeGeminiRequestOutput = SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func normalizedGeminiSignatureHistory(turns, totalPayloadBytes int) []byte {
|
||||
payload := strings.Repeat("x", totalPayloadBytes/turns)
|
||||
var builder strings.Builder
|
||||
builder.Grow(totalPayloadBytes + turns*256)
|
||||
builder.WriteString(`{"contents":[`)
|
||||
for i := 0; i < turns; i++ {
|
||||
if i > 0 {
|
||||
builder.WriteByte(',')
|
||||
}
|
||||
builder.WriteString(`{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{"value":"`)
|
||||
builder.WriteString(payload)
|
||||
builder.WriteString(`"}},"thoughtSignature":"`)
|
||||
builder.WriteString(GeminiSkipThoughtSignatureValidator)
|
||||
builder.WriteString(`"}]},{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"ok"}}}]}`)
|
||||
}
|
||||
builder.WriteString(`]}`)
|
||||
return []byte(builder.String())
|
||||
}
|
||||
|
||||
func TestSanitizeGeminiRequestThoughtSignaturesRemovesFunctionResponseSignature(t *testing.T) {
|
||||
input := []byte(`{"contents":[{"role":"user","parts":[{"functionResponse":{"name":"f","response":{"result":"ok"},"thoughtSignature":"bad","thoughtSignature":"worse"},"thoughtSignature":"bad"}]}]}`)
|
||||
|
||||
out := SanitizeGeminiRequestThoughtSignatures(input, "contents")
|
||||
|
||||
if gjson.GetBytes(out, "contents.0.parts.0.thoughtSignature").Exists() {
|
||||
t.Fatalf("functionResponse top-level thoughtSignature should be removed. Output: %s", string(out))
|
||||
}
|
||||
if gjson.GetBytes(out, "contents.0.parts.0.functionResponse.thoughtSignature").Exists() {
|
||||
t.Fatalf("functionResponse nested thoughtSignature should be removed. Output: %s", string(out))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue