@@ -2920,3 +2920,275 @@ func TestDefineDataPromptPanics(t *testing.T) {
29202920 }, "name is required" )
29212921 })
29222922}
2923+
2924+ // TestLoadPromptTemplateVariableSubstitution tests that template variables are
2925+ // properly substituted with actual input values at execution time.
2926+ // This is a regression test for https://github.com/firebase/genkit/issues/3924
2927+ func TestLoadPromptTemplateVariableSubstitution (t * testing.T ) {
2928+ t .Run ("single role" , func (t * testing.T ) {
2929+ tempDir := t .TempDir ()
2930+
2931+ mockPromptFile := filepath .Join (tempDir , "greeting.prompt" )
2932+ mockPromptContent := `---
2933+ model: test/chat
2934+ description: A greeting prompt with variables
2935+ ---
2936+ Hello {{name}}, welcome to {{place}}!
2937+ `
2938+
2939+ if err := os .WriteFile (mockPromptFile , []byte (mockPromptContent ), 0644 ); err != nil {
2940+ t .Fatalf ("Failed to create mock prompt file: %v" , err )
2941+ }
2942+
2943+ prompt := LoadPrompt (registry .New (), tempDir , "greeting.prompt" , "template-var-test" )
2944+
2945+ // Test with first set of input values
2946+ actionOpts1 , err := prompt .Render (context .Background (), map [string ]any {
2947+ "name" : "Alice" ,
2948+ "place" : "Wonderland" ,
2949+ })
2950+ if err != nil {
2951+ t .Fatalf ("Failed to render prompt with first input: %v" , err )
2952+ }
2953+
2954+ if len (actionOpts1 .Messages ) != 1 {
2955+ t .Fatalf ("Expected 1 message, got %d" , len (actionOpts1 .Messages ))
2956+ }
2957+
2958+ text1 := actionOpts1 .Messages [0 ].Content [0 ].Text
2959+ if ! strings .Contains (text1 , "Alice" ) {
2960+ t .Errorf ("Expected message to contain 'Alice', got: %s" , text1 )
2961+ }
2962+ if ! strings .Contains (text1 , "Wonderland" ) {
2963+ t .Errorf ("Expected message to contain 'Wonderland', got: %s" , text1 )
2964+ }
2965+
2966+ // Test with second set of input values (different from first)
2967+ actionOpts2 , err := prompt .Render (context .Background (), map [string ]any {
2968+ "name" : "Bob" ,
2969+ "place" : "Paradise" ,
2970+ })
2971+ if err != nil {
2972+ t .Fatalf ("Failed to render prompt with second input: %v" , err )
2973+ }
2974+
2975+ if len (actionOpts2 .Messages ) != 1 {
2976+ t .Fatalf ("Expected 1 message, got %d" , len (actionOpts2 .Messages ))
2977+ }
2978+
2979+ text2 := actionOpts2 .Messages [0 ].Content [0 ].Text
2980+ if ! strings .Contains (text2 , "Bob" ) {
2981+ t .Errorf ("Expected message to contain 'Bob', got: %s" , text2 )
2982+ }
2983+ if ! strings .Contains (text2 , "Paradise" ) {
2984+ t .Errorf ("Expected message to contain 'Paradise', got: %s" , text2 )
2985+ }
2986+
2987+ // Critical: Ensure the second render did NOT use the first input values
2988+ if strings .Contains (text2 , "Alice" ) {
2989+ t .Errorf ("BUG: Second render contains 'Alice' from first input! Got: %s" , text2 )
2990+ }
2991+ if strings .Contains (text2 , "Wonderland" ) {
2992+ t .Errorf ("BUG: Second render contains 'Wonderland' from first input! Got: %s" , text2 )
2993+ }
2994+ })
2995+
2996+ t .Run ("multi role" , func (t * testing.T ) {
2997+ tempDir := t .TempDir ()
2998+
2999+ mockPromptFile := filepath .Join (tempDir , "multi_role.prompt" )
3000+ mockPromptContent := `---
3001+ model: test/chat
3002+ description: A multi-role prompt with variables
3003+ ---
3004+ <<<dotprompt:role:system>>>
3005+ You are a {{personality}} assistant.
3006+
3007+ <<<dotprompt:role:user>>>
3008+ Hello {{name}}, please help me with {{task}}.
3009+ `
3010+
3011+ if err := os .WriteFile (mockPromptFile , []byte (mockPromptContent ), 0644 ); err != nil {
3012+ t .Fatalf ("Failed to create mock prompt file: %v" , err )
3013+ }
3014+
3015+ prompt := LoadPrompt (registry .New (), tempDir , "multi_role.prompt" , "multi-role-var-test" )
3016+
3017+ // Test with first set of input values
3018+ actionOpts1 , err := prompt .Render (context .Background (), map [string ]any {
3019+ "personality" : "helpful" ,
3020+ "name" : "Alice" ,
3021+ "task" : "coding" ,
3022+ })
3023+ if err != nil {
3024+ t .Fatalf ("Failed to render prompt with first input: %v" , err )
3025+ }
3026+
3027+ if len (actionOpts1 .Messages ) != 2 {
3028+ t .Fatalf ("Expected 2 messages, got %d" , len (actionOpts1 .Messages ))
3029+ }
3030+
3031+ // Check system message
3032+ systemMsg := actionOpts1 .Messages [0 ]
3033+ if systemMsg .Role != RoleSystem {
3034+ t .Errorf ("Expected first message role to be 'system', got '%s'" , systemMsg .Role )
3035+ }
3036+ systemText := systemMsg .Content [0 ].Text
3037+ if ! strings .Contains (systemText , "helpful" ) {
3038+ t .Errorf ("Expected system message to contain 'helpful', got: %s" , systemText )
3039+ }
3040+
3041+ // Check user message
3042+ userMsg := actionOpts1 .Messages [1 ]
3043+ if userMsg .Role != RoleUser {
3044+ t .Errorf ("Expected second message role to be 'user', got '%s'" , userMsg .Role )
3045+ }
3046+ userText := userMsg .Content [0 ].Text
3047+ if ! strings .Contains (userText , "Alice" ) {
3048+ t .Errorf ("Expected user message to contain 'Alice', got: %s" , userText )
3049+ }
3050+ if ! strings .Contains (userText , "coding" ) {
3051+ t .Errorf ("Expected user message to contain 'coding', got: %s" , userText )
3052+ }
3053+
3054+ // Test with second set of input values (different from first)
3055+ actionOpts2 , err := prompt .Render (context .Background (), map [string ]any {
3056+ "personality" : "professional" ,
3057+ "name" : "Bob" ,
3058+ "task" : "writing" ,
3059+ })
3060+ if err != nil {
3061+ t .Fatalf ("Failed to render prompt with second input: %v" , err )
3062+ }
3063+
3064+ if len (actionOpts2 .Messages ) != 2 {
3065+ t .Fatalf ("Expected 2 messages, got %d" , len (actionOpts2 .Messages ))
3066+ }
3067+
3068+ // Check system message with new values
3069+ systemMsg2 := actionOpts2 .Messages [0 ]
3070+ systemText2 := systemMsg2 .Content [0 ].Text
3071+ if ! strings .Contains (systemText2 , "professional" ) {
3072+ t .Errorf ("Expected system message to contain 'professional', got: %s" , systemText2 )
3073+ }
3074+ if strings .Contains (systemText2 , "helpful" ) {
3075+ t .Errorf ("BUG: Second render system message contains 'helpful' from first input! Got: %s" , systemText2 )
3076+ }
3077+
3078+ // Check user message with new values
3079+ userMsg2 := actionOpts2 .Messages [1 ]
3080+ userText2 := userMsg2 .Content [0 ].Text
3081+ if ! strings .Contains (userText2 , "Bob" ) {
3082+ t .Errorf ("Expected user message to contain 'Bob', got: %s" , userText2 )
3083+ }
3084+ if ! strings .Contains (userText2 , "writing" ) {
3085+ t .Errorf ("Expected user message to contain 'writing', got: %s" , userText2 )
3086+ }
3087+ if strings .Contains (userText2 , "Alice" ) {
3088+ t .Errorf ("BUG: Second render user message contains 'Alice' from first input! Got: %s" , userText2 )
3089+ }
3090+ if strings .Contains (userText2 , "coding" ) {
3091+ t .Errorf ("BUG: Second render user message contains 'coding' from first input! Got: %s" , userText2 )
3092+ }
3093+ })
3094+
3095+ t .Run ("multi role with handlebars syntax" , func (t * testing.T ) {
3096+ tempDir := t .TempDir ()
3097+
3098+ mockPromptFile := filepath .Join (tempDir , "handlebars_role.prompt" )
3099+ // Use Handlebars {{role "..."}} syntax instead of internal markers
3100+ mockPromptContent := `---
3101+ model: test/chat
3102+ description: A multi-role prompt using Handlebars role syntax
3103+ ---
3104+ {{role "system"}}
3105+ You are a {{personality}} assistant.
3106+
3107+ {{role "user"}}
3108+ Hello {{name}}, please help me with {{task}}.
3109+ `
3110+
3111+ if err := os .WriteFile (mockPromptFile , []byte (mockPromptContent ), 0644 ); err != nil {
3112+ t .Fatalf ("Failed to create mock prompt file: %v" , err )
3113+ }
3114+
3115+ prompt := LoadPrompt (registry .New (), tempDir , "handlebars_role.prompt" , "handlebars-role-test" )
3116+
3117+ // Test with first set of input values
3118+ actionOpts1 , err := prompt .Render (context .Background (), map [string ]any {
3119+ "personality" : "helpful" ,
3120+ "name" : "Alice" ,
3121+ "task" : "coding" ,
3122+ })
3123+ if err != nil {
3124+ t .Fatalf ("Failed to render prompt with first input: %v" , err )
3125+ }
3126+
3127+ if len (actionOpts1 .Messages ) != 2 {
3128+ t .Fatalf ("Expected 2 messages, got %d" , len (actionOpts1 .Messages ))
3129+ }
3130+
3131+ // Check system message
3132+ systemMsg := actionOpts1 .Messages [0 ]
3133+ if systemMsg .Role != RoleSystem {
3134+ t .Errorf ("Expected first message role to be 'system', got '%s'" , systemMsg .Role )
3135+ }
3136+ systemText := systemMsg .Content [0 ].Text
3137+ if ! strings .Contains (systemText , "helpful" ) {
3138+ t .Errorf ("Expected system message to contain 'helpful', got: %s" , systemText )
3139+ }
3140+
3141+ // Check user message
3142+ userMsg := actionOpts1 .Messages [1 ]
3143+ if userMsg .Role != RoleUser {
3144+ t .Errorf ("Expected second message role to be 'user', got '%s'" , userMsg .Role )
3145+ }
3146+ userText := userMsg .Content [0 ].Text
3147+ if ! strings .Contains (userText , "Alice" ) {
3148+ t .Errorf ("Expected user message to contain 'Alice', got: %s" , userText )
3149+ }
3150+ if ! strings .Contains (userText , "coding" ) {
3151+ t .Errorf ("Expected user message to contain 'coding', got: %s" , userText )
3152+ }
3153+
3154+ // Test with second set of input values (different from first)
3155+ actionOpts2 , err := prompt .Render (context .Background (), map [string ]any {
3156+ "personality" : "professional" ,
3157+ "name" : "Bob" ,
3158+ "task" : "writing" ,
3159+ })
3160+ if err != nil {
3161+ t .Fatalf ("Failed to render prompt with second input: %v" , err )
3162+ }
3163+
3164+ if len (actionOpts2 .Messages ) != 2 {
3165+ t .Fatalf ("Expected 2 messages, got %d" , len (actionOpts2 .Messages ))
3166+ }
3167+
3168+ // Check system message with new values
3169+ systemMsg2 := actionOpts2 .Messages [0 ]
3170+ systemText2 := systemMsg2 .Content [0 ].Text
3171+ if ! strings .Contains (systemText2 , "professional" ) {
3172+ t .Errorf ("Expected system message to contain 'professional', got: %s" , systemText2 )
3173+ }
3174+ if strings .Contains (systemText2 , "helpful" ) {
3175+ t .Errorf ("BUG: Second render system message contains 'helpful' from first input! Got: %s" , systemText2 )
3176+ }
3177+
3178+ // Check user message with new values
3179+ userMsg2 := actionOpts2 .Messages [1 ]
3180+ userText2 := userMsg2 .Content [0 ].Text
3181+ if ! strings .Contains (userText2 , "Bob" ) {
3182+ t .Errorf ("Expected user message to contain 'Bob', got: %s" , userText2 )
3183+ }
3184+ if ! strings .Contains (userText2 , "writing" ) {
3185+ t .Errorf ("Expected user message to contain 'writing', got: %s" , userText2 )
3186+ }
3187+ if strings .Contains (userText2 , "Alice" ) {
3188+ t .Errorf ("BUG: Second render user message contains 'Alice' from first input! Got: %s" , userText2 )
3189+ }
3190+ if strings .Contains (userText2 , "coding" ) {
3191+ t .Errorf ("BUG: Second render user message contains 'coding' from first input! Got: %s" , userText2 )
3192+ }
3193+ })
3194+ }
0 commit comments