vibe-proxy/backend/internal/signature/claude_test.go
2026-08-24 00:10:41 +02:00

641 lines
26 KiB
Go

package signature
import (
"encoding/base64"
"strings"
"testing"
"github.com/tidwall/gjson"
"google.golang.org/protobuf/encoding/protowire"
)
func TestStripInvalidClaudeThinkingBlocks_RemovesGPTEncryptedContent(t *testing.T) {
input := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"codex reasoning","signature":"gAAAAABopenai-encrypted-content"},
{"type":"text","text":"Answer"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
out := StripInvalidClaudeThinkingBlocks(input)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("messages.0.content length = %d, want 1: %s", len(content), string(out))
}
if got := content[0].Get("text").String(); got != "Answer" {
t.Fatalf("remaining content text = %q, want Answer", got)
}
if strings.Contains(string(out), "gAAAAABopenai-encrypted-content") || strings.Contains(string(out), "codex reasoning") {
t.Fatalf("invalid thinking block was preserved: %s", string(out))
}
}
func TestStripInvalidClaudeThinkingBlocksAndEmptyMessages_DropsMessagesLeftEmpty(t *testing.T) {
input := []byte(`{
"messages": [
{"role":"assistant","content":[
{"type":"thinking","thinking":"codex reasoning","signature":"gAAAAABopenai-encrypted-content"}
]},
{"role":"user","content":[{"type":"text","text":"next"}]}
]
}`)
out := StripInvalidClaudeThinkingBlocksAndEmptyMessages(input)
messages := gjson.GetBytes(out, "messages").Array()
if len(messages) != 1 {
t.Fatalf("messages length = %d, want 1: %s", len(messages), string(out))
}
if got := messages[0].Get("role").String(); got != "user" {
t.Fatalf("remaining role = %q, want user", got)
}
if strings.Contains(string(out), "gAAAAABopenai-encrypted-content") || strings.Contains(string(out), "codex reasoning") {
t.Fatalf("invalid thinking block was preserved: %s", string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_RemovesMalformedEPrefix(t *testing.T) {
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","thinking":"bad","signature":"Ebad"},
{"type":"text","text":"Answer"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("content length = %d, want 1: %s", len(content), string(out))
}
if strings.Contains(string(out), "Ebad") || strings.Contains(string(out), "bad") {
t.Fatalf("malformed E-prefix thinking block was preserved: %s", string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_Base64OnlyKeepsDecodableEPrefix(t *testing.T) {
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","thinking":"bad","signature":"Ebad"},
{"type":"text","text":"Answer"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input, ClaudeSignatureValidationOptions{Base64Only: true})
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("content length = %d, want 2: %s", len(content), string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_Base64OnlyRemovesInvalidBase64(t *testing.T) {
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","thinking":"bad","signature":"E!!!invalid!!!"},
{"type":"text","text":"Answer"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input, ClaudeSignatureValidationOptions{Base64Only: true})
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("content length = %d, want 1: %s", len(content), string(out))
}
if strings.Contains(string(out), "E!!!invalid!!!") || strings.Contains(string(out), "bad") {
t.Fatalf("invalid-base64 thinking block was preserved: %s", string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_AllowsEmptySignatureEmptyTextPlaceholder(t *testing.T) {
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","text":"","signature":""},
{"type":"text","text":"Answer"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input, ClaudeSignatureValidationOptions{
Base64Only: true,
AllowEmptySignatureWithEmptyText: true,
})
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("content length = %d, want 2: %s", len(content), string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_StrictRemovesMalformedClaudeTree(t *testing.T) {
sig := base64.StdEncoding.EncodeToString([]byte{0x12, 0xFF, 0xFE, 0xFD})
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","thinking":"bad","signature":"` + sig + `"},
{"type":"text","text":"Answer"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input, ClaudeSignatureValidationOptions{Strict: true})
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 1 {
t.Fatalf("content length = %d, want 1: %s", len(content), string(out))
}
if strings.Contains(string(out), sig) || strings.Contains(string(out), "bad") {
t.Fatalf("strict-invalid thinking block was preserved: %s", string(out))
}
}
func TestStripInvalidClaudeThinkingBlocks_KeepsClaudeSignaturePrefixes(t *testing.T) {
singleLayer := base64.StdEncoding.EncodeToString([]byte{0x12, 0x34})
doubleLayer := base64.StdEncoding.EncodeToString([]byte(singleLayer))
input := []byte(`{
"messages": [{"role":"assistant","content":[
{"type":"thinking","thinking":"one","signature":"` + singleLayer + `"},
{"type":"thinking","thinking":"two","signature":"modelGroup#` + doubleLayer + `"}
]}]
}`)
out := StripInvalidClaudeThinkingBlocks(input)
content := gjson.GetBytes(out, "messages.0.content").Array()
if len(content) != 2 {
t.Fatalf("content length = %d, want 2: %s", len(content), string(out))
}
}
const observedFable5Sample = "CAISqwIKiAEIEBgCKkBHRlRBsNiptQUWfPoOhuQKwi5LnncZVO9bB5jqOs76D7uBtgktML0zqJtNmLHXHHcgD6lk4MQu4QBXzFd1lbC3Mg5jbGF1ZGUtZmFibGUtNTgBQgh0aGlua2luZ1okZDk3NDM5NzUtNGJiMC00OTM2LTllMjgtZDViMGQyMWJkYzQ4EgxCGh+XVFFFeySAjtAaDL/A1LltGu6MMJ+eXSIwsN0oBpDrqLv22UBfkMnTotnIbkvkOyb9xZHgigG6OZVHaI3gThm+maLKmgO5PrFLKlDFYp+YZksy/wKwszJlnLTPzAK+NUlfzagOE1ymtZTXhAYK260XyFYmg/te/C231+Fr/hoX+EJoUBnrn0gD7hqMISOT+TaFEuOXYsN517GfaxgB"
const observedContextID = "d9743975-4bb0-4936-9e28-d5b0d21bdc48"
// claudeCAISParts builds Claude CAIS signatures field by field so tests can
// assert both the observed layout and the upstream drift the validator must
// tolerate or reject.
type claudeCAISParts struct {
includeTopEnvelope bool
topEnvelope uint64
includeTopTrailer bool
includeContainer bool
includeChannelBlock bool
includeChannelID bool
channelID uint64
channelIDAsBytes bool
includeChannelVerion bool
includeSignature bool
signatureLen int
includeModelText bool
modelText []byte
includeField7 bool
blockKind string
contextID string
}
// defaultClaudeCAISParts mirrors the layout observed on claude-fable-5 and
// claude-opus-5 responses.
func defaultClaudeCAISParts(model string) claudeCAISParts {
return claudeCAISParts{
includeTopEnvelope: true,
topEnvelope: 2,
includeTopTrailer: true,
includeContainer: true,
includeChannelBlock: true,
includeChannelID: true,
channelID: 16,
includeChannelVerion: true,
includeSignature: true,
signatureLen: 64,
includeModelText: true,
modelText: []byte(model),
includeField7: true,
blockKind: "thinking",
contextID: observedContextID,
}
}
func (p claudeCAISParts) encode() string {
var channelBlock []byte
if p.includeChannelID {
if p.channelIDAsBytes {
channelBlock = protowire.AppendTag(channelBlock, 1, protowire.BytesType)
channelBlock = protowire.AppendBytes(channelBlock, []byte{0x10})
} else {
channelBlock = protowire.AppendTag(channelBlock, 1, protowire.VarintType)
channelBlock = protowire.AppendVarint(channelBlock, p.channelID)
}
}
if p.includeChannelVerion {
channelBlock = protowire.AppendTag(channelBlock, 3, protowire.VarintType)
channelBlock = protowire.AppendVarint(channelBlock, 2)
}
if p.includeSignature {
channelBlock = protowire.AppendTag(channelBlock, 5, protowire.BytesType)
channelBlock = protowire.AppendBytes(channelBlock, make([]byte, p.signatureLen))
}
if p.includeModelText {
channelBlock = protowire.AppendTag(channelBlock, 6, protowire.BytesType)
channelBlock = protowire.AppendBytes(channelBlock, p.modelText)
}
if p.includeField7 {
channelBlock = protowire.AppendTag(channelBlock, 7, protowire.VarintType)
channelBlock = protowire.AppendVarint(channelBlock, 1)
}
if p.blockKind != "" {
channelBlock = protowire.AppendTag(channelBlock, 8, protowire.BytesType)
channelBlock = protowire.AppendString(channelBlock, p.blockKind)
}
if p.contextID != "" {
channelBlock = protowire.AppendTag(channelBlock, 11, protowire.BytesType)
channelBlock = protowire.AppendString(channelBlock, p.contextID)
}
var container []byte
if p.includeChannelBlock {
container = protowire.AppendTag(container, 1, protowire.BytesType)
container = protowire.AppendBytes(container, channelBlock)
}
var payload []byte
if p.includeTopEnvelope {
payload = protowire.AppendTag(payload, 1, protowire.VarintType)
payload = protowire.AppendVarint(payload, p.topEnvelope)
}
if p.includeContainer {
payload = protowire.AppendTag(payload, 2, protowire.BytesType)
payload = protowire.AppendBytes(payload, container)
}
if p.includeTopTrailer {
payload = protowire.AppendTag(payload, 3, protowire.VarintType)
payload = protowire.AppendVarint(payload, 1)
}
return base64.StdEncoding.EncodeToString(payload)
}
func testClaudeCAISSignature(model string) string {
return defaultClaudeCAISParts(model).encode()
}
func TestClaudeCAISSignature_ObservedFable5Sample(t *testing.T) {
if !IsValidClaudeCAISSignature(observedFable5Sample) {
t.Fatal("IsValidClaudeCAISSignature(observedFable5Sample) = false, want true")
}
info, err := InspectClaudeCAISSignature(observedFable5Sample)
if err != nil {
t.Fatalf("InspectClaudeCAISSignature failed: %v", err)
}
if info.ModelText != "claude-fable-5" {
t.Fatalf("ModelText = %q, want %q", info.ModelText, "claude-fable-5")
}
if info.BlockKind != "thinking" {
t.Fatalf("BlockKind = %q, want %q", info.BlockKind, "thinking")
}
expectedUUID := "d9743975-4bb0-4936-9e28-d5b0d21bdc48"
if info.ContextID != expectedUUID {
t.Fatalf("ContextID = %q, want %q", info.ContextID, expectedUUID)
}
if info.FirstByte != 0x08 {
t.Fatalf("FirstByte = 0x%02x, want 0x08", info.FirstByte)
}
}
func TestClaudeCAISSignature_DetectSignatureProvider(t *testing.T) {
prefixes := []string{
"",
"ccmax#",
"claude-code-max#",
"claude_code_max#",
"cais#",
"claude-cais#",
"claude_cais#",
"claude#",
}
for _, prefix := range prefixes {
sig := prefix + observedFable5Sample
got := DetectSignatureProvider(sig)
if got != SignatureProviderClaude {
t.Errorf("DetectSignatureProvider(%q) = %q, want %q", sig, got, SignatureProviderClaude)
}
}
}
func TestClaudeCAISSignature_ObservedOpus5Layout(t *testing.T) {
signature := testClaudeCAISSignature("claude-opus-5")
info, err := InspectClaudeCAISSignature(signature)
if err != nil {
t.Fatalf("InspectClaudeCAISSignature failed: %v", err)
}
if info.ModelText != "claude-opus-5" {
t.Fatalf("ModelText = %q, want claude-opus-5", info.ModelText)
}
decision := DecideSignatureCompatibilityForModel(SignatureProviderClaude, "claude-opus-5", signature, SignatureBlockKindClaudeThinking)
if !decision.Compatible || decision.NormalizedSignature != signature || decision.DetectedProvider != SignatureProviderClaude {
t.Fatalf("same-model opus-5 decision = %+v, want preserved with DetectedProvider=claude", decision)
}
}
func TestClaudeCAISSignature_NotCompatibleWithGemini(t *testing.T) {
if normalized, ok := CompatibleSignatureForProvider(SignatureProviderGemini, observedFable5Sample); ok || normalized != "" {
t.Fatalf("CompatibleSignatureForProvider(Gemini) = %q, %v; want empty and false", normalized, ok)
}
if IsSignatureCompatibleWithProvider(SignatureProviderGemini, observedFable5Sample) {
t.Fatal("IsSignatureCompatibleWithProvider(Gemini) = true, want false")
}
if isRecognizedGeminiProviderSignature(observedFable5Sample, SignatureBlockKindUnknown) {
t.Fatal("isRecognizedGeminiProviderSignature = true, want false")
}
if _, err := InspectGeminiThoughtSignature(observedFable5Sample); err == nil {
t.Fatal("InspectGeminiThoughtSignature should fail for Claude CAIS signature")
}
}
func TestClaudeCAISSignature_CompatibleWithAllClaudeTargets(t *testing.T) {
decision := DecideSignatureCompatibilityForModel(SignatureProviderClaude, "claude-fable-5", observedFable5Sample, SignatureBlockKindClaudeThinking)
if !decision.Compatible || decision.Action != SignatureActionPreserve || decision.NormalizedSignature != observedFable5Sample || decision.DetectedProvider != SignatureProviderClaude {
t.Fatalf("DecideSignatureCompatibilityForModel(Claude, claude-fable-5) = %+v, want compatible & preserved with DetectedProvider=claude", decision)
}
decisionCase := DecideSignatureCompatibilityForModel(SignatureProviderClaude, "CLAUDE-FABLE-5", observedFable5Sample, SignatureBlockKindClaudeThinking)
if !decisionCase.Compatible || decisionCase.Action != SignatureActionPreserve {
t.Fatalf("DecideSignatureCompatibilityForModel case-insensitive failed: %+v", decisionCase)
}
decisionDiff := DecideSignatureCompatibilityForModel(SignatureProviderClaude, "claude-opus-5", observedFable5Sample, SignatureBlockKindClaudeThinking)
if !decisionDiff.Compatible || decisionDiff.Action != SignatureActionPreserve || decisionDiff.NormalizedSignature != observedFable5Sample {
t.Fatalf("DecideSignatureCompatibilityForModel(Claude, claude-opus-5) = %+v, want compatible & preserved", decisionDiff)
}
opus5Sig := testClaudeCAISSignature("claude-opus-5")
decisionOpusToOpus48 := DecideSignatureCompatibilityForModel(SignatureProviderClaude, "claude-opus-4-8", opus5Sig, SignatureBlockKindClaudeThinking)
if !decisionOpusToOpus48.Compatible || decisionOpusToOpus48.Action != SignatureActionPreserve || decisionOpusToOpus48.NormalizedSignature != opus5Sig {
t.Fatalf("DecideSignatureCompatibilityForModel(Claude, claude-opus-4-8) with opus-5 signature = %+v, want compatible & preserved", decisionOpusToOpus48)
}
if normalized, ok := CompatibleSignatureForProvider(SignatureProviderClaude, observedFable5Sample); !ok || normalized != observedFable5Sample {
t.Fatalf("CompatibleSignatureForProvider(Claude, observedFable5Sample) = %q, %v; want %q, true", normalized, ok, observedFable5Sample)
}
decisionGemini := DecideSignatureCompatibilityForModel(SignatureProviderGemini, "claude-fable-5", observedFable5Sample, SignatureBlockKindClaudeThinking)
if decisionGemini.Compatible {
t.Fatalf("DecideSignatureCompatibilityForModel(Gemini, claude-fable-5) = %+v, want incompatible", decisionGemini)
}
}
func TestSanitizeClaudeMessagesForClaudeUpstream_ClaudeCAIS(t *testing.T) {
inputSame := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + observedFable5Sample + `"},{"type":"text","text":"answer"}]}]}`)
outputSame, reportSame := SanitizeClaudeMessagesForClaudeUpstream(inputSame, "claude-fable-5")
if reportSame.Preserved != 1 || reportSame.DroppedBlocks != 0 {
t.Fatalf("unexpected report for same model: %+v", reportSame)
}
if got := gjson.GetBytes(outputSame, "messages.0.content.0.signature").String(); got != observedFable5Sample {
t.Fatalf("signature = %q, want preserved %q", got, observedFable5Sample)
}
inputTool := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + observedFable5Sample + `"},{"type":"text","text":"answer"},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"pwd"},"signature":"` + observedFable5Sample + `"}]}]}`)
outputTool, reportTool := SanitizeClaudeMessagesForClaudeUpstream(inputTool, "claude-fable-5")
if reportTool.Preserved != 1 {
t.Fatalf("unexpected report for tool input: %+v", reportTool)
}
partsTool := gjson.GetBytes(outputTool, "messages.0.content").Array()
if len(partsTool) != 3 {
t.Fatalf("content len = %d, want 3", len(partsTool))
}
if partsTool[0].Get("signature").String() != observedFable5Sample {
t.Fatalf("thinking block signature lost: %s", partsTool[0].Raw)
}
if partsTool[2].Get("signature").Exists() {
t.Fatalf("tool_use signature should be stripped: %s", partsTool[2].Raw)
}
inputDiff := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + observedFable5Sample + `"},{"type":"text","text":"answer"}]}]}`)
outputDiff, reportDiff := SanitizeClaudeMessagesForClaudeUpstream(inputDiff, "claude-opus-5")
if reportDiff.Preserved != 1 || reportDiff.DroppedBlocks != 0 {
t.Fatalf("unexpected report for cross model: %+v", reportDiff)
}
partsDiff := gjson.GetBytes(outputDiff, "messages.0.content").Array()
if len(partsDiff) != 2 {
t.Fatalf("content len = %d, want 2: %s", len(partsDiff), outputDiff)
}
if got := partsDiff[0].Get("signature").String(); got != observedFable5Sample {
t.Fatalf("thinking signature = %q, want %q", got, observedFable5Sample)
}
}
// TestClaudeCAISSignature_ToleratesUpstreamFieldDrift pins the deliberately
// structural validation: rejecting a signature drops the whole thinking block,
// so incidental values observed today must not become hard requirements.
func TestClaudeCAISSignature_ToleratesUpstreamFieldDrift(t *testing.T) {
cases := []struct {
name string
parts claudeCAISParts
}{
{"observed layout", defaultClaudeCAISParts("claude-opus-5")},
{"new channel id", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.channelID = 17
return p
}()},
{"new envelope version", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.topEnvelope = 3
return p
}()},
{"no top-level trailer", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeTopTrailer = false
return p
}()},
{"no channel version", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeChannelVerion = false
return p
}()},
{"longer signature bytes", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.signatureLen = 96
return p
}()},
{"no field 7", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeField7 = false
return p
}()},
{"other block kind", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.blockKind = "redacted_thinking"
return p
}()},
{"no block kind", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.blockKind = ""
return p
}()},
{"no context id", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-5")
p.contextID = ""
return p
}()},
{"unreleased model name", func() claudeCAISParts {
p := defaultClaudeCAISParts("claude-opus-6-preview")
return p
}()},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
sig := tc.parts.encode()
if _, err := InspectClaudeCAISSignature(sig); err != nil {
t.Fatalf("InspectClaudeCAISSignature failed: %v", err)
}
if got := DetectSignatureProviderForBlock(sig, SignatureBlockKindClaudeThinking); got != SignatureProviderClaude {
t.Fatalf("DetectSignatureProviderForBlock = %q, want %q", got, SignatureProviderClaude)
}
input := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + sig + `"}]}]}`)
output, report := SanitizeClaudeMessagesForClaudeUpstream(input, "claude-opus-5")
if report.Preserved != 1 || report.DroppedBlocks != 0 {
t.Fatalf("report = %+v, want preserved thinking block", report)
}
if got := gjson.GetBytes(output, "messages.0.content.0.signature").String(); got != sig {
t.Fatalf("signature = %q, want preserved %q", got, sig)
}
})
}
}
func TestClaudeCAISSignature_RejectsMalformedPayloads(t *testing.T) {
truncated := func() string {
decoded, err := base64.StdEncoding.DecodeString(observedFable5Sample)
if err != nil {
t.Fatalf("decode observed sample: %v", err)
}
return base64.StdEncoding.EncodeToString(decoded[:len(decoded)/2])
}()
cases := []struct {
name string
signature string
}{
{"empty", ""},
{"whitespace only", " "},
{"not base64", "CAIS!!!not-base64"},
{"truncated payload", truncated},
// 'E' prefix is the classic Claude form and must not reach CAIS parsing.
{"classic claude prefix", base64.StdEncoding.EncodeToString([]byte{0x12, 0x00})},
// 'C' prefix but a non-0x08 marker byte, the only way to reach the marker
// check (a 'C' prefix constrains the first byte to 0x08-0x0b).
{"wrong marker byte", base64.StdEncoding.EncodeToString([]byte{0x0a, 0x00})},
{"no container", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeContainer = false
return p.encode()
}()},
{"no channel block", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeChannelBlock = false
return p.encode()
}()},
{"no channel id", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeChannelID = false
return p.encode()
}()},
{"channel id wrong wire type", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.channelIDAsBytes = true
return p.encode()
}()},
{"no signature bytes", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeSignature = false
return p.encode()
}()},
{"empty signature bytes", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.signatureLen = 0
return p.encode()
}()},
{"no model text", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.includeModelText = false
return p.encode()
}()},
{"foreign model text", func() string {
p := defaultClaudeCAISParts("gemini-3-pro")
return p.encode()
}()},
{"invalid utf-8 model text", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.modelText = []byte{'c', 'l', 'a', 'u', 'd', 'e', '-', 0xff, 0xfe}
return p.encode()
}()},
{"non-uuid context id", func() string {
p := defaultClaudeCAISParts("claude-opus-5")
p.contextID = "not-a-canonical-uuid-value-000000000"
return p.encode()
}()},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if IsValidClaudeCAISSignature(tc.signature) {
t.Fatalf("IsValidClaudeCAISSignature(%q) = true, want false", tc.signature)
}
})
}
}
// TestClaudeCAISSignature_DoesNotShadowClassicClaudeSignature guards the
// detection order: CAIS validation runs before classic Claude validation, so it
// must not claim E/R signatures and change how they are normalized.
func TestClaudeCAISSignature_DoesNotShadowClassicClaudeSignature(t *testing.T) {
classic := testClaudeThinkingSignature()
if IsValidClaudeCAISSignature(classic) {
t.Fatal("IsValidClaudeCAISSignature(classic Claude signature) = true, want false")
}
if got := DetectSignatureProviderForBlock(classic, SignatureBlockKindClaudeThinking); got != SignatureProviderClaude {
t.Fatalf("DetectSignatureProviderForBlock(classic) = %q, want %q", got, SignatureProviderClaude)
}
input := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + classic + `"}]}]}`)
output, report := SanitizeClaudeMessagesForClaudeUpstream(input, "claude-sonnet-4-6")
if report.Preserved != 1 || report.DroppedBlocks != 0 {
t.Fatalf("report = %+v, want preserved classic thinking block", report)
}
if got := gjson.GetBytes(output, "messages.0.content.0.signature").String(); got != classic {
t.Fatalf("signature = %q, want provider-native E-form %q", got, classic)
}
}
// TestClaudeCAISSignature_CachePrefixSurvivesClaudeUpstreamSanitize covers the
// cached-signature path: cache.GetModelGroup collapses every Claude model to the
// "claude" prefix, so a CAIS signature reaches the sanitizer as "claude#..." and
// must be replayed with the prefix stripped instead of being dropped.
func TestClaudeCAISSignature_CachePrefixSurvivesClaudeUpstreamSanitize(t *testing.T) {
for _, prefix := range []string{"claude#", "anthropic#", "cais#", "ccmax#"} {
t.Run(prefix, func(t *testing.T) {
prefixed := prefix + observedFable5Sample
input := []byte(`{"messages":[{"role":"assistant","content":[{"type":"thinking","thinking":"keep","signature":"` + prefixed + `"}]}]}`)
output, report := SanitizeClaudeMessagesForClaudeUpstream(input, "claude-fable-5")
if report.Preserved != 1 || report.DroppedBlocks != 0 {
t.Fatalf("report = %+v, want preserved thinking block", report)
}
if got := gjson.GetBytes(output, "messages.0.content.0.signature").String(); got != observedFable5Sample {
t.Fatalf("signature = %q, want unprefixed %q", got, observedFable5Sample)
}
})
}
}
func TestCompatibleAntigravityClaudeThinkingSignature_RejectsClaudeCAIS(t *testing.T) {
if normalized, ok := CompatibleAntigravityClaudeThinkingSignature(observedFable5Sample); ok || normalized != "" {
t.Fatalf("CompatibleAntigravityClaudeThinkingSignature(ClaudeCAIS) = %q, %v; want empty and false", normalized, ok)
}
if normalized, ok := CompatibleAntigravityClaudeThinkingSignature("ccmax#" + observedFable5Sample); ok || normalized != "" {
t.Fatalf("CompatibleAntigravityClaudeThinkingSignature(ccmax#ClaudeCAIS) = %q, %v; want empty and false", normalized, ok)
}
if normalized, ok := CompatibleAntigravityClaudeThinkingSignature("cais#" + observedFable5Sample); ok || normalized != "" {
t.Fatalf("CompatibleAntigravityClaudeThinkingSignature(cais#ClaudeCAIS) = %q, %v; want empty and false", normalized, ok)
}
if normalized, ok := CompatibleAntigravityClaudeThinkingSignature("claude-cais#" + observedFable5Sample); ok || normalized != "" {
t.Fatalf("CompatibleAntigravityClaudeThinkingSignature(claude-cais#ClaudeCAIS) = %q, %v; want empty and false", normalized, ok)
}
}