| |
| |
| package logging |
|
|
| import ( |
| "bytes" |
| "strings" |
| "testing" |
| ) |
|
|
| |
| |
| func FuzzSanitizeBytes(f *testing.F) { |
| |
| f.Add([]byte(`{"thinking": "some secret thought"}`)) |
| f.Add([]byte(`{"arguments": "secret args"}`)) |
| f.Add([]byte(`{"thinking": "thought", "arguments": "args"}`)) |
| f.Add([]byte(`{"message": {"thinking": "nested thought"}}`)) |
| f.Add([]byte(`{}`)) |
| f.Add([]byte(`[]`)) |
| f.Add([]byte(``)) |
| f.Add([]byte(`{"thinking": ""}`)) |
| f.Add([]byte(`{"thinking": "multi\nline\ncontent"}`)) |
| f.Add([]byte(`{"thinking": "escaped \"quotes\" here"}`)) |
| f.Add([]byte(`{"other": "thinking: not redacted"}`)) |
| f.Add([]byte(`{"thinking": "value", "other": "data", "arguments": "more"}`)) |
|
|
| f.Fuzz(func(t *testing.T, data []byte) { |
| result := sanitizeBytes(data) |
|
|
| |
| resultStr := string(result) |
|
|
| |
| |
| if strings.Contains(resultStr, `"thinking"`) { |
| |
| |
| if strings.Contains(resultStr, `"thinking": "`) && |
| !strings.Contains(resultStr, `"thinking": "[REDACTED]"`) { |
| |
| |
| idx := strings.Index(resultStr, `"thinking"`) |
| if idx >= 0 { |
| afterKey := resultStr[idx+len(`"thinking"`):] |
| afterKey = strings.TrimLeft(afterKey, " \t\n\r") |
| if strings.HasPrefix(afterKey, `:`) { |
| afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") |
| if strings.HasPrefix(afterColon, `"`) { |
| |
| endQuote := strings.Index(afterColon[1:], `"`) |
| if endQuote > 0 { |
| value := afterColon[1 : endQuote+1] |
| if value != "[REDACTED]" && value != "" { |
| t.Errorf("thinking value not redacted: got %q", value) |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| if strings.Contains(resultStr, `"arguments"`) { |
| if strings.Contains(resultStr, `"arguments": "`) && |
| !strings.Contains(resultStr, `"arguments": "[REDACTED]"`) { |
| idx := strings.Index(resultStr, `"arguments"`) |
| if idx >= 0 { |
| afterKey := resultStr[idx+len(`"arguments"`):] |
| afterKey = strings.TrimLeft(afterKey, " \t\n\r") |
| if strings.HasPrefix(afterKey, `:`) { |
| afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") |
| if strings.HasPrefix(afterColon, `"`) { |
| endQuote := strings.Index(afterColon[1:], `"`) |
| if endQuote > 0 { |
| value := afterColon[1 : endQuote+1] |
| if value != "[REDACTED]" && value != "" { |
| t.Errorf("arguments value not redacted: got %q", value) |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
|
|
| |
| |
| inputStr := string(data) |
| if strings.Contains(inputStr, `"thinking"`) { |
| |
| thinkingVal := extractJSONStringValue(inputStr, "thinking") |
| if thinkingVal != "" && thinkingVal != "[REDACTED]" { |
| if strings.Contains(resultStr, thinkingVal) && !strings.Contains(dataStrWithoutKey(resultStr, "thinking"), thinkingVal) { |
| t.Errorf("original thinking value %q still present in output", thinkingVal) |
| } |
| } |
| } |
| if strings.Contains(inputStr, `"arguments"`) { |
| argsVal := extractJSONStringValue(inputStr, "arguments") |
| if argsVal != "" && argsVal != "[REDACTED]" { |
| if strings.Contains(resultStr, argsVal) && !strings.Contains(dataStrWithoutKey(resultStr, "arguments"), argsVal) { |
| t.Errorf("original arguments value %q still present in output", argsVal) |
| } |
| } |
| } |
|
|
| |
| if result == nil && len(data) > 0 { |
| t.Error("sanitizeBytes returned nil for non-empty input") |
| } |
| }) |
| } |
|
|
| |
| func FuzzSanitizeBytesStructured(f *testing.F) { |
| |
| f.Add([]byte(`{ |
| "model": "gpt-4", |
| "thinking": "This is my internal reasoning process", |
| "messages": [{"role": "user", "content": "Hello"}] |
| }`)) |
|
|
| |
| f.Add([]byte(`{ |
| "function": "calculate", |
| "arguments": "{\"x\": 10, \"y\": 20}", |
| "other": "data" |
| }`)) |
|
|
| |
| f.Add([]byte(`{ |
| "thinking": "secret thought", |
| "arguments": "secret args", |
| "output": "public result" |
| }`)) |
|
|
| |
| f.Add([]byte(`{"nested": {"thinking": "nested value"}}`)) |
|
|
| |
| f.Add([]byte(`[{"thinking": "first"}, {"thinking": "second"}]`)) |
|
|
| f.Fuzz(func(t *testing.T, data []byte) { |
| result := sanitizeBytes(data) |
|
|
| |
| if len(result) > len(data)*2 { |
| t.Logf("Result significantly larger than input: input=%d, output=%d", len(data), len(result)) |
| } |
|
|
| |
| _ = result |
| }) |
| } |
|
|
| |
| func FuzzSanitizeBytesEdgeCases(f *testing.F) { |
| |
| |
| |
|
|
| |
| f.Add([]byte(`{`)) |
|
|
| |
| f.Add([]byte(`thinking`)) |
| f.Add([]byte(`arguments`)) |
|
|
| |
| f.Add([]byte(`{"thinking" : "value"}`)) |
| f.Add([]byte(`{"thinking":"value"}`)) |
| f.Add([]byte(`{"thinking" : "value"}`)) |
|
|
| |
| f.Add([]byte(`{"thinking": "value with \"quotes\""}`)) |
|
|
| |
| f.Add([]byte(`{"thinking": "日本語テキスト"}`)) |
| f.Add([]byte(`{"thinking": "🎉 emoji test"}`)) |
|
|
| |
| longValue := bytes.Repeat([]byte("a"), 10000) |
| f.Add(append([]byte(`{"thinking": "`), append(longValue, []byte(`"}`)...)...)) |
|
|
| |
| f.Add([]byte{0x00, 0x01, 0x02, 0x03}) |
|
|
| f.Fuzz(func(t *testing.T, data []byte) { |
| |
| result := sanitizeBytes(data) |
|
|
| |
| |
| if result != nil { |
| _ = len(result) |
| } |
| }) |
| } |
|
|
| |
| func extractJSONStringValue(jsonStr, key string) string { |
| keyPattern := `"` + key + `"` |
| idx := strings.Index(jsonStr, keyPattern) |
| if idx < 0 { |
| return "" |
| } |
|
|
| afterKey := jsonStr[idx+len(keyPattern):] |
| afterKey = strings.TrimLeft(afterKey, " \t\n\r") |
|
|
| if !strings.HasPrefix(afterKey, `:`) { |
| return "" |
| } |
|
|
| afterColon := strings.TrimLeft(afterKey[1:], " \t\n\r") |
| if !strings.HasPrefix(afterColon, `"`) { |
| return "" |
| } |
|
|
| |
| start := 1 |
| for i := start; i < len(afterColon); i++ { |
| if afterColon[i] == '"' && (i == 0 || afterColon[i-1] != '\\') { |
| return afterColon[start:i] |
| } |
| } |
|
|
| return "" |
| } |
|
|
| |
| func dataStrWithoutKey(dataStr, key string) string { |
| keyPattern := `"` + key + `"` |
| idx := strings.Index(dataStr, keyPattern) |
| if idx < 0 { |
| return dataStr |
| } |
|
|
| |
| return dataStr[:idx] |
| } |
|
|
| |
| func TestSanitizeBytesSpecificCases(t *testing.T) { |
| tests := []struct { |
| name string |
| input string |
| expected string |
| }{ |
| { |
| name: "simple thinking", |
| input: `{"thinking": "secret"}`, |
| expected: `{"thinking": "[REDACTED]"}`, |
| }, |
| { |
| name: "simple arguments", |
| input: `{"arguments": "secret"}`, |
| expected: `{"arguments": "[REDACTED]"}`, |
| }, |
| { |
| name: "both fields", |
| input: `{"thinking": "thought", "arguments": "args"}`, |
| expected: `{"thinking": "[REDACTED]", "arguments": "[REDACTED]"}`, |
| }, |
| { |
| name: "with whitespace", |
| input: `{"thinking" : "secret"}`, |
| expected: `{"thinking": "[REDACTED]"}`, |
| }, |
| { |
| name: "escaped quotes", |
| input: `{"thinking": "has \"quotes\""}`, |
| expected: `{"thinking": "[REDACTED]"}`, |
| }, |
| { |
| name: "empty value", |
| input: `{"thinking": ""}`, |
| expected: `{"thinking": "[REDACTED]"}`, |
| }, |
| { |
| name: "no match", |
| input: `{"other": "thinking: not redacted"}`, |
| expected: `{"other": "thinking: not redacted"}`, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| result := sanitizeBytes([]byte(tt.input)) |
| if string(result) != tt.expected { |
| t.Errorf("sanitizeBytes() = %q, want %q", string(result), tt.expected) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestSanitizeBytesPreservesStructure(t *testing.T) { |
| inputs := []string{ |
| `{"model": "gpt-4", "thinking": "reasoning", "messages": []}`, |
| `[{"thinking": "first"}, {"thinking": "second"}]`, |
| `{"nested": {"thinking": "value"}}`, |
| `{"thinking": "a", "other": "b", "arguments": "c"}`, |
| } |
|
|
| for _, input := range inputs { |
| result := sanitizeBytes([]byte(input)) |
|
|
| |
| resultStr := string(result) |
|
|
| |
| openBraces := strings.Count(resultStr, "{") |
| closeBraces := strings.Count(resultStr, "}") |
| if openBraces != closeBraces { |
| t.Errorf("Mismatched braces in: %s", resultStr) |
| } |
|
|
| |
| openBrackets := strings.Count(resultStr, "[") |
| closeBrackets := strings.Count(resultStr, "]") |
| if openBrackets != closeBrackets { |
| t.Errorf("Mismatched brackets in: %s", resultStr) |
| } |
|
|
| |
| quotes := strings.Count(resultStr, `"`) |
| if quotes%2 != 0 { |
| t.Errorf("Unmatched quotes in: %s", resultStr) |
| } |
| } |
| } |
|
|