Resolve direct prompt input names

This commit is contained in:
2026-10-08 20:36:17 +00:00
parent 93e8874813
commit 089e824a92
4 changed files with 336 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"reflect"
"strings"
"testing"
"text/template"
@@ -360,6 +361,177 @@ func TestAnalyzeRequirementsRetainsInactiveEntriesAndPromotesActiveUses(t *testi
}
}
func TestAnalyzeRequirementsResolvesSimpleInputNames(t *testing.T) {
tests := []struct {
name string
expression string
vars map[string]string
kind domain.RequirementKind
supplied bool
}{
{name: "direct dot", expression: `{{input .name}}`, vars: map[string]string{"name": " chosen "}, kind: domain.RequirementRequired, supplied: true},
{name: "explicit root", expression: `{{input $.name}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "parenthesized direct", expression: `{{input (.name)}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "literal lookup", expression: `{{input (index . "name")}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementOptional, supplied: true},
{name: "direct pipeline", expression: `{{.name | input}}`, vars: map[string]string{"name": "chosen"}, kind: domain.RequirementRequired, supplied: true},
{name: "empty supplied value", expression: `{{input .name}}`, vars: map[string]string{"name": ""}, kind: domain.RequirementRequired, supplied: true},
{name: "missing lookup has empty zero value", expression: `{{input (index . "name")}}`, vars: nil, kind: domain.RequirementOptional, supplied: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
definition := &domain.PromptDefinition{Templates: []domain.PromptMessageTemplate{{Role: "user", Content: test.expression}}}
snapshot := RequirementSnapshot{Templates: CompiledTemplates{Messages: []*template.Template{
compileRequirementTemplate(t, "message", test.expression),
}}}
result, err := AnalyzeRequirements(context.Background(), definition, snapshot, test.vars, false)
if err != nil {
t.Fatal(err)
}
inputName := "chosen"
if test.name == "direct dot" {
inputName = " chosen "
}
if test.name == "empty supplied value" || test.name == "missing lookup has empty zero value" {
inputName = ""
}
if !result.Complete || len(result.Issues) != 0 || len(result.Inputs) != 1 || result.Inputs[0].Name != inputName ||
result.Inputs[0].Kind != domain.RequirementRequired {
t.Fatalf("simple input name was not resolved exactly: %+v", result)
}
if len(result.Variables) != 1 || result.Variables[0].Name != "name" || result.Variables[0].Kind != test.kind || result.Variables[0].Supplied != test.supplied {
t.Fatalf("name-source caller key classification is incorrect: %+v", result.Variables)
}
artifacts := map[string]*domain.Artifact{inputName: {Name: inputName, Body: []byte("resolved")}}
rendered, err := NewGoRenderer().Render(context.Background(), definition, artifacts, test.vars)
if err != nil {
t.Fatal(err)
}
if len(rendered.Messages) != 1 || rendered.Messages[0].Content != "resolved" {
t.Fatalf("analyzed input name differs from renderer: %+v", rendered)
}
})
}
}
func TestAnalyzeRequirementsReportsUnresolvedInputNamesAndPromotesUnknownEffects(t *testing.T) {
definition := &domain.PromptDefinition{Templates: []domain.PromptMessageTemplate{{
Role: "user", Content: `{{input .missing}}`,
}}}
result, err := AnalyzeRequirements(context.Background(), definition, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "optional"}, {Name: "required", Required: true}},
Templates: CompiledTemplates{Messages: []*template.Template{
compileRequirementTemplate(t, "message", definition.Templates[0].Content),
}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Issues) != 2 || result.Issues[0].Code != domain.RequirementIssueDynamicInputName ||
result.Issues[1].Code != domain.RequirementIssueMissingVariable {
t.Fatalf("missing dynamic name did not report its limitations: %+v", result)
}
if result.Inputs[0].Name != "optional" || result.Inputs[0].Kind != domain.RequirementConditional || result.Inputs[0].Explanation == "" ||
result.Inputs[1].Name != "required" || result.Inputs[1].Kind != domain.RequirementRequired || len(result.Variables) != 1 ||
result.Variables[0].Name != "missing" || result.Variables[0].Kind != domain.RequirementRequired {
t.Fatalf("unknown name effects lost known classifications: %+v", result)
}
computed := compileRequirementTemplate(t, "computed", `{{input (printf "%s" .prefix)}}`)
computedResult, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "declared"}}, Templates: CompiledTemplates{Messages: []*template.Template{computed}},
}, map[string]string{"prefix": "SENSITIVE-VALUE"}, false)
if err != nil {
t.Fatal(err)
}
if computedResult.Complete || len(computedResult.Inputs) != 1 || computedResult.Inputs[0].Name != "declared" ||
computedResult.Inputs[0].Kind != domain.RequirementConditional || len(computedResult.Variables) != 1 ||
computedResult.Variables[0].Name != "prefix" || computedResult.Variables[0].Kind != domain.RequirementConditional {
t.Fatalf("computed-name uncertainty did not affect optional entries: %+v", computedResult)
}
for _, issue := range computedResult.Issues {
if strings.Contains(issue.Description, "SENSITIVE-VALUE") || issue.InputName == "SENSITIVE-VALUE" || issue.VariableName == "SENSITIVE-VALUE" {
t.Fatalf("input-name source value leaked into diagnostics: %+v", issue)
}
}
for _, input := range computedResult.Inputs {
if strings.Contains(input.Explanation, "SENSITIVE-VALUE") {
t.Fatalf("input-name source value leaked into an explanation: %+v", input)
}
}
for _, variable := range computedResult.Variables {
if strings.Contains(variable.Explanation, "SENSITIVE-VALUE") {
t.Fatalf("input-name source value leaked into an explanation: %+v", variable)
}
}
for _, expression := range []string{`{{input (input "selector")}}`, `{{input "selector" | input}}`} {
t.Run(expression, func(t *testing.T) {
contentName := compileRequirementTemplate(t, "content-name", expression)
contentResult, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{contentName}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if contentResult.Complete || len(contentResult.Issues) != 2 ||
contentResult.Issues[0].Code != domain.RequirementIssueDynamicInputName ||
contentResult.Issues[1].Code != domain.RequirementIssueInputContent || contentResult.Issues[1].InputName != "selector" || len(contentResult.Inputs) != 1 ||
contentResult.Inputs[0].Name != "selector" || contentResult.Inputs[0].Kind != domain.RequirementRequired {
t.Fatalf("input-body-derived name did not preserve the known source dependency: %+v", contentResult)
}
})
}
}
func TestAnalyzeRequirementsDoesNotResolveSkippedComputedInputNames(t *testing.T) {
compiled := compileRequirementTemplate(t, "inactive-name", `{{if false}}{{input .name}}{{input (printf "%s" .other)}}{{"piped-literal" | input}}{{template "missing" .arg}}{{end}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if !result.Complete || len(result.Issues) != 0 || len(result.Inputs) != 1 || result.Inputs[0].Name != "piped-literal" ||
result.Inputs[0].Kind != domain.RequirementOptional || result.Inputs[0].Explanation == "" || len(result.Variables) != 3 {
t.Fatalf("skipped computed expressions introduced uncertainty or names: %+v", result)
}
for _, variable := range result.Variables {
if variable.Kind != domain.RequirementOptional || variable.Explanation == "" {
t.Errorf("skipped variable %q was not retained as inactive optional: %+v", variable.Name, variable)
}
}
}
func TestAnalyzeRequirementsPromotesOptionalVariablesWhenRootScopeIsUnknown(t *testing.T) {
compiled := compileRequirementTemplate(t, "scope", `{{index . "optional"}} {{.required}} {{template "called" .}}{{define "called"}}{{$.hidden}}{{end}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, map[string]string{"required": "value"}, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Variables) != 2 || result.Variables[0].Name != "optional" ||
result.Variables[0].Kind != domain.RequirementConditional || result.Variables[0].Explanation == "" ||
result.Variables[1].Name != "required" || result.Variables[1].Kind != domain.RequirementRequired {
t.Fatalf("unknown caller scope did not affect only optional known entries: %+v", result)
}
}
func TestAnalyzeRequirementsLeavesAssignmentDerivedNamesPartial(t *testing.T) {
compiled := compileRequirementTemplate(t, "assignment", `{{$alias := .source}}{{input $alias}}`)
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Inputs: []InputMetadata{{Name: "known-optional"}}, Templates: CompiledTemplates{Messages: []*template.Template{compiled}},
}, nil, false)
if err != nil {
t.Fatal(err)
}
if result.Complete || len(result.Issues) == 0 || len(result.Inputs) != 1 ||
result.Inputs[0].Name != "known-optional" || result.Inputs[0].Kind != domain.RequirementConditional ||
len(result.Variables) != 1 || result.Variables[0].Name != "source" || result.Variables[0].Kind != domain.RequirementConditional {
t.Fatalf("assignment-derived input name was interpreted or lost: %+v", result)
}
}
func TestAnalyzeRequirementsSupportsEmptyTemplatesAndCancellation(t *testing.T) {
result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{
Templates: CompiledTemplates{Messages: []*template.Template{compileRequirementTemplate(t, "message", "plain text")}},