assemble_test.go
1 package agent 2 3 import ( 4 "strings" 5 "testing" 6 7 "github.com/Kocoro-lab/ShanClaw/internal/prompt" 8 ) 9 10 // TestAssembleUserMessage_InstructionsOnlyEmitsCacheBreak is the end-to-end 11 // guard for the instructions-in-StableContext move: when only shared 12 // instructions are present (no sticky facts), the assembled user message must 13 // still emit the <!-- cache_break --> marker with instructions sitting in the 14 // cacheable prefix. This is what locks in the "caching win" contract; a unit 15 // test on buildStableContext alone would pass even if assembleUserMessage 16 // regressed to skip the marker. 17 func TestAssembleUserMessage_InstructionsOnlyEmitsCacheBreak(t *testing.T) { 18 parts := prompt.BuildSystemPrompt(prompt.PromptOptions{ 19 BasePrompt: "You are Shannon.", 20 Instructions: "Never push to main without review.", 21 }) 22 23 result := assembleUserMessage(parts, "ship the release") 24 25 idx := strings.Index(result, "<!-- cache_break -->") 26 if idx < 0 { 27 t.Fatalf("expected cache_break marker, got:\n%s", result) 28 } 29 30 prefix := result[:idx] 31 suffix := result[idx:] 32 33 if !strings.Contains(prefix, "## Instructions") { 34 t.Error("Instructions header should be in the cached prefix (before cache_break)") 35 } 36 if !strings.Contains(prefix, "Never push to main without review.") { 37 t.Error("instructions body should be in the cached prefix") 38 } 39 if strings.Contains(suffix, "Never push to main without review.") { 40 t.Error("instructions body must not appear after cache_break") 41 } 42 if !strings.HasSuffix(result, "ship the release") { 43 t.Error("raw user message should be at the end") 44 } 45 } 46 47 func TestAssembleUserMessage_CacheBreakRegression(t *testing.T) { 48 t.Run("empty stable omits marker", func(t *testing.T) { 49 result := assembleUserMessage(prompt.PromptParts{ 50 StableContext: "", 51 VolatileContext: "current date: 2026-04-03", 52 }, "hello") 53 if strings.Contains(result, "cache_break") { 54 t.Error("cache_break should not appear when StableContext is empty") 55 } 56 }) 57 58 t.Run("non-empty stable includes marker", func(t *testing.T) { 59 result := assembleUserMessage(prompt.PromptParts{ 60 StableContext: "system instructions", 61 VolatileContext: "current date: 2026-04-03", 62 }, "hello") 63 if !strings.Contains(result, "cache_break") { 64 t.Error("cache_break should appear when StableContext is non-empty") 65 } 66 }) 67 68 t.Run("marker separates stable from volatile", func(t *testing.T) { 69 result := assembleUserMessage(prompt.PromptParts{ 70 StableContext: "stable-prefix", 71 VolatileContext: "volatile-suffix", 72 }, "user-query") 73 74 idx := strings.Index(result, "<!-- cache_break -->") 75 if idx < 0 { 76 t.Fatal("marker not found") 77 } 78 if !strings.Contains(result[:idx], "stable-prefix") { 79 t.Error("stable content should be before marker") 80 } 81 if !strings.Contains(result[idx:], "volatile-suffix") { 82 t.Error("volatile content should be after marker") 83 } 84 if !strings.HasSuffix(result, "user-query") { 85 t.Error("user message should be at the end") 86 } 87 }) 88 } 89 90 func TestAssembleUserMessage_SessionPlaceholderEmitsCacheBreak(t *testing.T) { 91 parts := prompt.PromptParts{ 92 System: "static", 93 StableContext: "## Session\nActive agent context.", 94 VolatileContext: "## Context\nDate: 2026-04-14", 95 } 96 msg := assembleUserMessage(parts, "hello") 97 if !strings.Contains(msg, "<!-- cache_break -->") { 98 t.Fatalf("cache_break marker missing when only session placeholder present") 99 } 100 if !strings.Contains(msg, "Active agent context.") { 101 t.Fatalf("session placeholder not preserved: %q", msg) 102 } 103 }