diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 3dd4ca5e..82b12a2f 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -1266,6 +1266,10 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte { } return true }) + } else if system.Type == gjson.String && system.String() != "" { + partJSON := `{"type":"text","cache_control":{"type":"ephemeral"}}` + partJSON, _ = sjson.Set(partJSON, "text", system.String()) + result += "," + partJSON } result += "]" diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index ead4e299..7bf77a7a 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -980,3 +980,87 @@ func TestClaudeExecutor_ExecuteStream_GzipErrorBodyNoContentEncodingHeader(t *te t.Errorf("error message should contain decompressed JSON, got: %q", err.Error()) } } + +// Test case 1: String system prompt is preserved and converted to a content block +func TestCheckSystemInstructionsWithMode_StringSystemPreserved(t *testing.T) { + payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`) + + out := checkSystemInstructionsWithMode(payload, false) + + system := gjson.GetBytes(out, "system") + if !system.IsArray() { + t.Fatalf("system should be an array, got %s", system.Type) + } + + blocks := system.Array() + if len(blocks) != 3 { + t.Fatalf("expected 3 system blocks, got %d", len(blocks)) + } + + if !strings.HasPrefix(blocks[0].Get("text").String(), "x-anthropic-billing-header:") { + t.Fatalf("blocks[0] should be billing header, got %q", blocks[0].Get("text").String()) + } + if blocks[1].Get("text").String() != "You are a Claude agent, built on Anthropic's Claude Agent SDK." { + t.Fatalf("blocks[1] should be agent block, got %q", blocks[1].Get("text").String()) + } + if blocks[2].Get("text").String() != "You are a helpful assistant." { + t.Fatalf("blocks[2] should be user system prompt, got %q", blocks[2].Get("text").String()) + } + if blocks[2].Get("cache_control.type").String() != "ephemeral" { + t.Fatalf("blocks[2] should have cache_control.type=ephemeral") + } +} + +// Test case 2: Strict mode drops the string system prompt +func TestCheckSystemInstructionsWithMode_StringSystemStrict(t *testing.T) { + payload := []byte(`{"system":"You are a helpful assistant.","messages":[{"role":"user","content":"hi"}]}`) + + out := checkSystemInstructionsWithMode(payload, true) + + blocks := gjson.GetBytes(out, "system").Array() + if len(blocks) != 2 { + t.Fatalf("strict mode should produce 2 blocks, got %d", len(blocks)) + } +} + +// Test case 3: Empty string system prompt does not produce a spurious block +func TestCheckSystemInstructionsWithMode_EmptyStringSystemIgnored(t *testing.T) { + payload := []byte(`{"system":"","messages":[{"role":"user","content":"hi"}]}`) + + out := checkSystemInstructionsWithMode(payload, false) + + blocks := gjson.GetBytes(out, "system").Array() + if len(blocks) != 2 { + t.Fatalf("empty string system should produce 2 blocks, got %d", len(blocks)) + } +} + +// Test case 4: Array system prompt is unaffected by the string handling +func TestCheckSystemInstructionsWithMode_ArraySystemStillWorks(t *testing.T) { + payload := []byte(`{"system":[{"type":"text","text":"Be concise."}],"messages":[{"role":"user","content":"hi"}]}`) + + out := checkSystemInstructionsWithMode(payload, false) + + blocks := gjson.GetBytes(out, "system").Array() + if len(blocks) != 3 { + t.Fatalf("expected 3 system blocks, got %d", len(blocks)) + } + if blocks[2].Get("text").String() != "Be concise." { + t.Fatalf("blocks[2] should be user system prompt, got %q", blocks[2].Get("text").String()) + } +} + +// Test case 5: Special characters in string system prompt survive conversion +func TestCheckSystemInstructionsWithMode_StringWithSpecialChars(t *testing.T) { + payload := []byte(`{"system":"Use tags & \"quotes\" in output.","messages":[{"role":"user","content":"hi"}]}`) + + out := checkSystemInstructionsWithMode(payload, false) + + blocks := gjson.GetBytes(out, "system").Array() + if len(blocks) != 3 { + t.Fatalf("expected 3 system blocks, got %d", len(blocks)) + } + if blocks[2].Get("text").String() != `Use tags & "quotes" in output.` { + t.Fatalf("blocks[2] text mangled, got %q", blocks[2].Get("text").String()) + } +}