From a2459931b6a8346d1fdb1b4a6c3b70785c6d3d1c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 5 May 2026 10:39:10 -0500 Subject: [PATCH] Expand PromptDefinition renderer coverage and tighten missing/nil input handling --- internal/prompt/go_renderer.go | 2 +- internal/prompt/renderer_test.go | 204 +++++++++++++++++++++++-------- 2 files changed, 154 insertions(+), 52 deletions(-) diff --git a/internal/prompt/go_renderer.go b/internal/prompt/go_renderer.go index fb0bb09..92c273d 100644 --- a/internal/prompt/go_renderer.go +++ b/internal/prompt/go_renderer.go @@ -43,7 +43,7 @@ func (r *goRenderer) Render(ctx context.Context, definition *domain.PromptDefini funcs := template.FuncMap{ "input": func(name string) (string, error) { art, ok := inputs[name] - if !ok { + if !ok || art == nil { return "", fmt.Errorf("%w: %s", ErrUnknownInput, name) } return string(art.Body), nil diff --git a/internal/prompt/renderer_test.go b/internal/prompt/renderer_test.go index 5925366..ca3c2a3 100644 --- a/internal/prompt/renderer_test.go +++ b/internal/prompt/renderer_test.go @@ -12,99 +12,201 @@ func TestGoRenderer_Render(t *testing.T) { renderer := NewGoRenderer() ctx := context.Background() - profile := &domain.PromptDefinition{ - ID: "test-profile", - Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, - Templates: []domain.PromptMessageTemplate{ - {Role: "system", Content: "You are a {{.role}}."}, - {Role: "user", Content: "Analyze this: {{input \"transcript\"}}"}, - }, - } - inputs := map[string]*domain.Artifact{ "transcript": {Body: []byte("The quick brown fox.")}, } - vars := map[string]string{ "role": "helpful assistant", + "tone": "concise", } - t.Run("successful render", func(t *testing.T) { - res, err := renderer.Render(ctx, profile, inputs, vars) + t.Run("rendering inline message content", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "Analyze this: {{input \"transcript\"}}"}, + }, + } + + res, err := renderer.Render(ctx, def, inputs, vars) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(res.Messages)) + } + if res.Messages[0].Content != "Analyze this: The quick brown fox." { + t.Fatalf("unexpected rendered content: %q", res.Messages[0].Content) + } + }) + + t.Run("rendering file-backed message content loaded into prompt definition", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "From file: {{input \"transcript\"}}", ContentFile: "/tmp/user.tmpl"}, + }, + } + + res, err := renderer.Render(ctx, def, inputs, vars) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := res.Messages[0].Content; got != "From file: The quick brown fox." { + t.Fatalf("unexpected file-backed render result: %q", got) + } + }) + + t.Run("rendering system and user messages", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "system", Content: "You are a {{.role}}."}, + {Role: "user", Content: "Analyze this: {{input \"transcript\"}}"}, + }, + } + + res, err := renderer.Render(ctx, def, inputs, vars) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(res.Messages) != 2 { - t.Errorf("expected 2 messages, got %d", len(res.Messages)) + t.Fatalf("expected 2 messages, got %d", len(res.Messages)) } - if res.Messages[0].Content != "You are a helpful assistant." { - t.Errorf("unexpected system message: %s", res.Messages[0].Content) - } - if res.Messages[1].Content != "Analyze this: The quick brown fox." { - t.Errorf("unexpected user message: %s", res.Messages[1].Content) + if res.Messages[0].Role != "system" || res.Messages[1].Role != "user" { + t.Fatalf("unexpected roles: %#v", res.Messages) } }) - t.Run("missing required input", func(t *testing.T) { - emptyInputs := map[string]*domain.Artifact{} - _, err := renderer.Render(ctx, profile, emptyInputs, vars) - if !errors.Is(err, ErrMissingRequiredInput) { - t.Errorf("expected ErrMissingRequiredInput, got %v", err) - } - }) - - t.Run("unknown input in template", func(t *testing.T) { - profileUnknown := &domain.PromptDefinition{ + t.Run("accessing vars", func(t *testing.T) { + def := &domain.PromptDefinition{ Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, Templates: []domain.PromptMessageTemplate{ - {Role: "user", Content: "Hello {{input \"ghost\"}}"}, + {Role: "system", Content: "Speak in a {{.tone}} tone."}, }, } - _, err := renderer.Render(ctx, profileUnknown, inputs, vars) + + res, err := renderer.Render(ctx, def, inputs, vars) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Messages[0].Content != "Speak in a concise tone." { + t.Fatalf("unexpected vars rendering: %q", res.Messages[0].Content) + } + }) + + t.Run("inserting required input artifact", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "{{input \"transcript\"}}"}, + }, + } + + res, err := renderer.Render(ctx, def, inputs, vars) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Messages[0].Content != "The quick brown fox." { + t.Fatalf("unexpected required input rendering: %q", res.Messages[0].Content) + } + }) + + t.Run("optional input absent and not referenced", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{ + {Name: "transcript", Required: true}, + {Name: "glossary", Required: false}, + }, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "Transcript: {{input \"transcript\"}}"}, + }, + } + + res, err := renderer.Render(ctx, def, inputs, vars) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Messages) != 1 { + t.Fatalf("expected one rendered message, got %d", len(res.Messages)) + } + }) + + t.Run("optional input absent but referenced, expecting failure", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{ + {Name: "transcript", Required: true}, + {Name: "glossary", Required: false}, + }, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "Glossary: {{input \"glossary\"}}"}, + }, + } + + _, err := renderer.Render(ctx, def, inputs, vars) if !errors.Is(err, ErrRenderFailure) { - t.Errorf("expected ErrRenderFailure, got %v", err) + t.Fatalf("expected ErrRenderFailure, got %v", err) } if !errors.Is(err, ErrUnknownInput) { - t.Errorf("expected ErrUnknownInput, got %v", err) + t.Fatalf("expected ErrUnknownInput, got %v", err) + } + }) + + t.Run("required input missing, expecting failure", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "Analyze this: {{input \"transcript\"}}"}, + }, + } + + _, err := renderer.Render(ctx, def, map[string]*domain.Artifact{}, vars) + if !errors.Is(err, ErrMissingRequiredInput) { + t.Fatalf("expected ErrMissingRequiredInput, got %v", err) } }) t.Run("invalid template syntax", func(t *testing.T) { - profileInvalid := &domain.PromptDefinition{ + def := &domain.PromptDefinition{ Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, Templates: []domain.PromptMessageTemplate{ {Role: "user", Content: "Hello {{.unclosed"}, }, } - _, err := renderer.Render(ctx, profileInvalid, inputs, vars) + + _, err := renderer.Render(ctx, def, inputs, vars) if !errors.Is(err, ErrInvalidTemplate) { - t.Errorf("expected ErrInvalidTemplate, got %v", err) + t.Fatalf("expected ErrInvalidTemplate, got %v", err) + } + }) + + t.Run("unknown input reference", func(t *testing.T) { + def := &domain.PromptDefinition{ + Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, + Templates: []domain.PromptMessageTemplate{ + {Role: "user", Content: "Hello {{input \"ghost\"}}"}, + }, + } + + _, err := renderer.Render(ctx, def, inputs, vars) + if !errors.Is(err, ErrRenderFailure) { + t.Fatalf("expected ErrRenderFailure, got %v", err) + } + if !errors.Is(err, ErrUnknownInput) { + t.Fatalf("expected ErrUnknownInput, got %v", err) } }) t.Run("empty message role", func(t *testing.T) { - profileNoRole := &domain.PromptDefinition{ + def := &domain.PromptDefinition{ Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, Templates: []domain.PromptMessageTemplate{ {Role: "", Content: "Hello"}, }, } - _, err := renderer.Render(ctx, profileNoRole, inputs, vars) + _, err := renderer.Render(ctx, def, inputs, vars) if !errors.Is(err, ErrInvalidMessageRole) { - t.Errorf("expected ErrInvalidMessageRole, got %v", err) - } - }) - - t.Run("missing variable in template", func(t *testing.T) { - profileMissingVar := &domain.PromptDefinition{ - Inputs: []domain.PromptInput{{Name: "transcript", Required: true}}, - Templates: []domain.PromptMessageTemplate{ - {Role: "system", Content: "You are {{.missing}}"}, - }, - } - _, err := renderer.Render(ctx, profileMissingVar, inputs, vars) - if !errors.Is(err, ErrRenderFailure) { - t.Errorf("expected ErrRenderFailure for missing variable, got %v", err) + t.Fatalf("expected ErrInvalidMessageRole, got %v", err) } }) }