Store one prompt definition per ID

This commit is contained in:
2026-10-17 05:41:21 +00:00
parent 0cd833513c
commit d40a010538
28 changed files with 393 additions and 445 deletions

View File

@@ -31,6 +31,7 @@ type configurationShape struct {
func benchmarkPrompt(profileID string) map[string]any {
return map[string]any{
"version": "1",
"default_profile": profileID,
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.name}}"}},
"output": map[string]any{"format": "text", "validation_mode": "none"},
@@ -44,7 +45,7 @@ func independentDefinitions(count int) configurationShape {
profileID := fmt.Sprintf("profile-%03d", index)
promptID := fmt.Sprintf("prompt-%03d", index)
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
prompts[promptID] = map[string]any{"1": benchmarkPrompt(profileID)}
prompts[promptID] = benchmarkPrompt(profileID)
}
return configurationShape{
main: configkit.Input{Source: configkit.Mapping("independent definitions", map[string]any{"profiles": profiles, "prompts": prompts})},
@@ -63,7 +64,7 @@ func fileBackedDefinitions(count int) configurationShape {
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
prompt := benchmarkPrompt(profileID)
prompt["messages"] = []any{map[string]any{"role": "user", "content_file": "body.md"}}
prompts[promptID] = map[string]any{"1": prompt}
prompts[promptID] = prompt
}
namespace := configkit.NewFilesystem("benchmark content", fstest.MapFS{
"assets/body.md": &fstest.MapFile{Data: []byte("Hello {{.name}}")},
@@ -89,7 +90,7 @@ func sharedBaseDefinitions(count int) configurationShape {
return configurationShape{
main: configkit.Input{Source: configkit.Mapping("inherited definitions", map[string]any{
"profiles": profiles,
"prompts": map[string]any{"shared": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))}},
"prompts": map[string]any{"shared": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))},
})},
inputEntries: count + 2,
effectiveEntries: count + 2,
@@ -117,7 +118,7 @@ func layeredDefinitions(count int) configurationShape {
},
main: configkit.Input{Source: configkit.Mapping("main", map[string]any{
"profiles": final,
"prompts": map[string]any{"layered": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))}},
"prompts": map[string]any{"layered": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))},
})},
inputEntries: count*4 + 1,
effectiveEntries: count + 1,
@@ -160,7 +161,7 @@ func BenchmarkConfigurationSelection(b *testing.B) {
b.Run("prompt", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
definition, _, err := result.prompt("prompt-127", "1")
definition, _, err := result.prompt("prompt-127")
if err != nil {
b.Fatal(err)
}
@@ -201,7 +202,7 @@ func inspectionDefinitions(extraParams map[string]any) configurationShape {
prompt := benchmarkPrompt("small")
prompt["inputs"] = []any{map[string]any{"name": "transcript", "required": true}}
prompts := map[string]any{
"inspection": map[string]any{"1": prompt},
"inspection": prompt,
}
return configurationShape{
main: configkit.Input{Source: configkit.Mapping("inspection definitions", map[string]any{

View File

@@ -22,12 +22,9 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
" z/profile ": map[string]any{"backend": "local", "model": "spaced-model"},
},
"prompts": map[string]any{
"z/prompt": map[string]any{"v1": listingPromptDefinition()},
"a/prompt": map[string]any{"v1": listingPromptDefinition()},
" z/prompt ": map[string]any{
"2": listingPromptDefinition(),
"10": listingPromptDefinition(),
},
"z/prompt": listingPromptDefinition(),
"a/prompt": listingPromptDefinition(),
" z/prompt ": listingPromptDefinition(),
},
}), nil)
if err != nil {
@@ -48,8 +45,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
t.Fatal(err)
}
wantPrompts := []domain.PromptIdentity{
{ID: " z/prompt ", Version: "10"},
{ID: " z/prompt ", Version: "2"},
{ID: " z/prompt ", Version: "v1"},
{ID: "a/prompt", Version: "v1"},
{ID: "z/prompt", Version: "v1"},
}
@@ -61,7 +57,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
func TestLoadedListsAreIndependent(t *testing.T) {
state, err := Load(mapping("identities", map[string]any{
"profiles": map[string]any{"profile": map[string]any{}},
"prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}},
"prompts": map[string]any{"example": listingPromptDefinition()},
}), nil)
if err != nil {
t.Fatal(err)
@@ -116,7 +112,7 @@ func TestLoadedListsEmptyInventory(t *testing.T) {
func TestLoadedListsHonorCanceledContext(t *testing.T) {
state, err := Load(mapping("identities", map[string]any{
"profiles": map[string]any{"partial": map[string]any{}},
"prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}},
"prompts": map[string]any{"example": listingPromptDefinition()},
}), nil)
if err != nil {
t.Fatal(err)
@@ -136,6 +132,7 @@ func TestLoadedListsHonorCanceledContext(t *testing.T) {
func listingPromptDefinition() map[string]any {
return map[string]any{
"version": "v1",
"messages": []any{map[string]any{"role": "user", "content": "Hello."}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}

View File

@@ -17,9 +17,9 @@ import (
)
type document struct {
Backends map[string]backendConfig `yaml:"backends"`
Profiles map[string]profileConfig `yaml:"profiles"`
Prompts map[string]map[string]promptConfig `yaml:"prompts"`
Backends map[string]backendConfig `yaml:"backends"`
Profiles map[string]profileConfig `yaml:"profiles"`
Prompts map[string]promptConfig `yaml:"prompts"`
}
type backendConfig struct {
@@ -62,6 +62,7 @@ type profileConfig struct {
}
type promptConfig struct {
Version *string `yaml:"version"`
DefaultProfile *string `yaml:"default_profile"`
Description *string `yaml:"description"`
SessionID *string `yaml:"session_id"`
@@ -110,9 +111,8 @@ type loaded struct {
result *configkit.Result[document]
backends map[string]map[string]fieldState
profiles map[string]map[string]fieldState
promptMetadata map[string]map[string][]promptInputPresence
prompts map[string]map[string]*promptEntry
ambiguous map[string]bool
promptMetadata map[string][]promptInputPresence
prompts map[string]*promptEntry
registry *backend.Registry
indexedProfiles map[string]domain.ExecutionProfile
profileIDs []string
@@ -149,7 +149,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
state := &loaded{
backends: make(map[string]map[string]fieldState),
profiles: make(map[string]map[string]fieldState),
promptMetadata: make(map[string]map[string][]promptInputPresence),
promptMetadata: make(map[string][]promptInputPresence),
}
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{
Underlays: underlays,
@@ -181,9 +181,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
}
state.profileIDs = sortedKeys(state.indexedProfiles)
for _, id := range sortedKeys(state.prompts) {
for _, version := range sortedKeys(state.prompts[id]) {
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: version})
}
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: state.prompts[id].definition.Version})
}
return state, nil
}
@@ -193,24 +191,19 @@ type promptInputPresence struct {
description fieldState
}
func capturePromptMetadata(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) {
func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) {
for _, id := range sortedKeys(prompts) {
if destination[id] == nil {
destination[id] = make(map[string][]promptInputPresence, len(prompts[id]))
}
for _, version := range sortedKeys(prompts[id]) {
inputs := prompts[id][version].Inputs
states := make([]promptInputPresence, len(inputs))
base := basePath.Key(id).Key(version).Key("inputs")
for index := range inputs {
path := base.Index(index)
states[index] = promptInputPresence{
contentType: captureFieldState(path.Key("content_type"), resolution),
description: captureFieldState(path.Key("description"), resolution),
}
inputs := prompts[id].Inputs
states := make([]promptInputPresence, len(inputs))
base := basePath.Key(id).Key("inputs")
for index := range inputs {
path := base.Index(index)
states[index] = promptInputPresence{
contentType: captureFieldState(path.Key("content_type"), resolution),
description: captureFieldState(path.Key("description"), resolution),
}
destination[id][version] = states
}
destination[id] = states
}
}

View File

@@ -21,10 +21,11 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
"extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}},
"child.v1": map[string]any{"extends": "base/one"},
},
"prompts": map[string]any{"invoice/summary": map[string]any{"v1.0": map[string]any{
"prompts": map[string]any{"invoice/summary": map[string]any{
"version": "v1.0",
"messages": []any{map[string]any{"role": "user", "content": "Summarize."}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}},
}},
})
overlay := mapping("operator", map[string]any{
"backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}},
@@ -54,7 +55,7 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue {
t.Fatalf("explicit zero queue capacity was lost: %+v", backend)
}
if loaded.result.Config.Prompts["invoice/summary"]["v1.0"].Messages[0].Content == nil {
if loaded.result.Config.Prompts["invoice/summary"].Messages[0].Content == nil || loaded.result.Config.Prompts["invoice/summary"].Version == nil || *loaded.result.Config.Prompts["invoice/summary"].Version != "v1.0" {
t.Fatal("literal prompt ID and version were not decoded")
}
}

View File

@@ -16,8 +16,7 @@ import (
)
var (
errPromptNotFound = errors.New("prompt definition not found")
errPromptAmbiguous = errors.New("prompt version is ambiguous")
errPromptNotFound = errors.New("prompt definition not found")
)
type promptEntry struct {
@@ -29,64 +28,61 @@ type promptEntry struct {
messages []*template.Template
}
func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
func resolvePrompts(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
for _, id := range sortedKeys(prompts) {
for _, version := range sortedKeys(prompts[id]) {
definition := prompts[id][version]
base := basePath.Key(id).Key(version)
definition := prompts[id]
base := basePath.Key(id)
for _, field := range []struct {
name string
value **string
}{
{"description", &definition.Description},
{"session_id", &definition.SessionID},
} {
if *field.value != nil {
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
}
}
inputsChanged := false
for index := range definition.Inputs {
input := &definition.Inputs[index]
inputPath := base.Key("inputs").Index(index)
normalizedName := strings.TrimSpace(input.Name)
if normalizedName != input.Name {
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
inputsChanged = true
}
for _, field := range []struct {
name string
value **string
}{
{"description", &definition.Description},
{"session_id", &definition.SessionID},
{"content_type", &input.ContentType},
{"description", &input.Description},
} {
if *field.value != nil {
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
}
}
inputsChanged := false
for index := range definition.Inputs {
input := &definition.Inputs[index]
inputPath := base.Key("inputs").Index(index)
normalizedName := strings.TrimSpace(input.Name)
if normalizedName != input.Name {
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
inputsChanged = true
}
for _, field := range []struct {
name string
value **string
}{
{"content_type", &input.ContentType},
{"description", &input.Description},
} {
if *field.value != nil {
normalized := strings.TrimSpace(**field.value)
if normalized != **field.value {
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
inputsChanged = true
}
normalized := strings.TrimSpace(**field.value)
if normalized != **field.value {
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
inputsChanged = true
}
}
}
if inputsChanged {
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata")
}
// Normalize message metadata on execution-owned copies below. ConfigKit
// records sequence mutations as whole-value replacements, which would
// discard the effective source contexts of unchanged content_file fields.
if definition.Output != nil && definition.Output.SchemaPath != nil {
normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath))
}
prompts[id][version] = definition
}
if inputsChanged {
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata")
}
// Normalize message metadata on execution-owned copies below. ConfigKit
// records sequence mutations as whole-value replacements, which would
// discard the effective source contexts of unchanged content_file fields.
if definition.Output != nil && definition.Output.SchemaPath != nil {
normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath))
}
prompts[id] = definition
}
}
func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
state.ambiguous = make(map[string]bool)
func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path) error {
state.prompts = make(map[string]*promptEntry, len(prompts))
var metadata *configkit.Metadata
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
if metadata == nil {
@@ -100,23 +96,15 @@ func (state *loaded) materializePrompts(prompts map[string]map[string]promptConf
if strings.TrimSpace(id) == "" {
return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
}
versions := prompts[id]
if versions == nil {
return fmt.Errorf("%s: prompt collection must be a mapping", promptBase)
version := ""
if prompts[id].Version != nil {
version = *prompts[id].Version
}
state.prompts[id] = make(map[string]*promptEntry, len(versions))
state.ambiguous[id] = len(versions) > 1
for _, version := range sortedKeys(versions) {
versionBase := promptBase.Key(version)
if strings.TrimSpace(version) == "" {
return fmt.Errorf("%s: prompt version must not be blank", versionBase)
}
entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin)
if err != nil {
return err
}
state.prompts[id][version] = entry
entry, err := state.materializePrompt(id, version, prompts[id], promptBase, lookupOrigin)
if err != nil {
return err
}
state.prompts[id] = entry
}
return nil
}
@@ -164,7 +152,7 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, bas
definition.Inputs = append(definition.Inputs, domain.PromptInput{
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
})
presence := state.promptMetadata[id][version][index]
presence := state.promptMetadata[id][index]
metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required}
if presence.contentType == fieldValue {
metadata.ContentType = stringPointer(contentType)
@@ -299,19 +287,10 @@ func (state *loaded) readContent(fieldPath configkit.Path, name string, lookupOr
}
func (state *loaded) lookupPromptEntry(id, version string) (*promptEntry, error) {
if state == nil || strings.TrimSpace(id) == "" || (version != "" && strings.TrimSpace(version) == "") {
if state == nil || strings.TrimSpace(id) == "" {
return nil, errPromptNotFound
}
versions := state.prompts[id]
if version == "" {
if state.ambiguous[id] {
return nil, errPromptAmbiguous
}
for selected := range versions {
version = selected
}
}
entry := versions[version]
entry := state.prompts[id]
if entry == nil {
return nil, errPromptNotFound
}
@@ -342,8 +321,8 @@ func promptLookupError(err error) error {
return err
}
func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domain.PromptInspection, error) {
entry, err := state.lookupPromptEntry(id, version)
func (state *loaded) prompt(id string) (*domain.PromptDefinition, domain.PromptInspection, error) {
entry, err := state.lookupPromptEntry(id, "")
if err != nil {
return nil, domain.PromptInspection{}, err
}
@@ -351,7 +330,8 @@ func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domai
}
// GetPromptSnapshot returns an independently owned captured definition and its
// construction-time hash. Lookup retains literal ID and version keys.
// construction-time hash. The version argument is retained temporarily at the
// adapter boundary and does not participate in lookup.
func (state *loaded) GetPromptSnapshot(ctx context.Context, id, version string) (*domain.PromptDefinition, string, error) {
if err := ctx.Err(); err != nil {
return nil, "", err
@@ -382,12 +362,12 @@ func (state *loaded) GetPromptDefinition(ctx context.Context, id, version string
return definition, err
}
// GetCompiledTemplates returns construction-time parses for one exact prompt.
// GetCompiledTemplates returns construction-time parses for one prompt ID.
func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTemplates, bool) {
if state == nil {
return prompt.CompiledTemplates{}, false
}
entry := state.prompts[id][version]
entry := state.prompts[id]
if entry == nil {
return prompt.CompiledTemplates{}, false
}
@@ -395,13 +375,13 @@ func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTe
}
// GetRequirementSnapshot returns captured templates and input metadata for
// one exact prompt identity. Returned metadata and slices are independently
// one prompt ID. Returned metadata and slices are independently
// owned; the compiled templates are shared immutable construction-time parses.
func (state *loaded) GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool) {
if state == nil {
return prompt.RequirementSnapshot{}, false
}
entry := state.prompts[id][version]
entry := state.prompts[id]
if entry == nil {
return prompt.RequirementSnapshot{}, false
}

View File

@@ -17,7 +17,7 @@ import (
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
files := fstest.MapFS{
"app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")},
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n v1:\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n version: \"v1\"\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
}
namespace := configkit.NewFilesystem("embedded", files)
@@ -32,7 +32,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
}
t.Fatal(err)
}
definition, inspection, err := state.prompt("child", "")
definition, inspection, err := state.prompt("child")
if err != nil {
t.Fatal(err)
}
@@ -43,7 +43,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" {
t.Fatalf("message metadata was not normalized: %+v", message)
}
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "v1", "messages").Index(0).Key("content_file"))
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "messages").Index(0).Key("content_file"))
if !ok || len(field.Origin.Locations) != 1 {
t.Fatalf("content origin was lost during normalization: %+v", field)
}
@@ -57,12 +57,12 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
t.Fatalf("content origin = %q, want catalog/base.yaml", originPath)
}
files["catalog/body.md"].Data = []byte("Changed")
again, againInspection, err := state.prompt("child", "v1")
again, againInspection, err := state.prompt("child")
if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash {
t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err)
}
definition.Templates[0].Content = "caller mutation"
third, _, err := state.prompt("child", "v1")
third, _, err := state.prompt("child")
if err != nil || third.Templates[0].Content != "Captured {{.name}}" {
t.Fatalf("caller mutation escaped into index: %+v, %v", third, err)
}
@@ -81,12 +81,12 @@ func TestLoadUsesExplicitMappingContextAndClosesContent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
definition, _, err := state.prompt("example", "v1")
definition, _, err := state.prompt("example")
if err != nil || definition.Templates[0].Content != "From mapping" || filesystem.contentOpened != 1 || filesystem.contentClosed != 1 {
t.Fatalf("mapping context or handle ownership failed: %+v, opens=%d closes=%d, %v", definition, filesystem.contentOpened, filesystem.contentClosed, err)
}
_, err = load(mapping("contextless", promptWithFile("body.md")), nil)
if err == nil || !strings.Contains(err.Error(), "/prompts/example/v1/messages/0/content_file") {
if err == nil || !strings.Contains(err.Error(), "/prompts/example/messages/0/content_file") {
t.Fatalf("expected semantic path for contextless resource, got %v", err)
}
}
@@ -95,7 +95,7 @@ func TestLoadCapturesNativeContentFile(t *testing.T) {
root := t.TempDir()
definitionPath := filepath.Join(root, "definition.yaml")
contentPath := filepath.Join(root, "body.md")
if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n v1:\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil {
if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n version: \"v1\"\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(contentPath, []byte("Native body"), 0o600); err != nil {
@@ -108,31 +108,31 @@ func TestLoadCapturesNativeContentFile(t *testing.T) {
if err := os.WriteFile(contentPath, []byte("Modified"), 0o600); err != nil {
t.Fatal(err)
}
definition, _, err := state.prompt("example", "v1")
definition, _, err := state.prompt("example")
if err != nil || definition.Templates[0].Content != "Native body" {
t.Fatalf("native content was not captured: %+v, %v", definition, err)
}
}
func TestLoadRejectsMalformedStaticSyntaxAndAmbiguousSelection(t *testing.T) {
func TestLoadRejectsMalformedStaticSyntaxAndCapturesOptionalVersion(t *testing.T) {
_, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{
"v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "{{if}}"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
"messages": []any{map[string]any{"role": "user", "content": "{{if}}"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}}), nil)
if err == nil || !strings.Contains(err.Error(), "invalid template syntax") {
t.Fatalf("expected construction-time syntax error, got %v", err)
}
state, err := load(mapping("versions", map[string]any{"prompts": map[string]any{"example": map[string]any{
"v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "one"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
"v2": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "two"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
state, err := load(mapping("version metadata", map[string]any{"prompts": map[string]any{"example": map[string]any{
"version": "release label",
"messages": []any{map[string]any{"role": "user", "content": "one"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}}), nil)
if err != nil {
t.Fatal(err)
}
if _, _, err := state.prompt("example", ""); !errors.Is(err, errPromptAmbiguous) {
t.Fatalf("expected ambiguous version selection, got %v", err)
definition, inspection, err := state.prompt("example")
if err != nil || definition.Version != "release label" || inspection.PromptVersion != "release label" {
t.Fatalf("optional version metadata was not retained: definition=%+v inspection=%+v err=%v", definition, inspection, err)
}
}
@@ -148,30 +148,26 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing.
"output": map[string]any{"format": "text", "validation_mode": "none"},
}
}
selectedPrompt := makePrompt("selected")
selectedPrompt["version"] = "v2"
state, err := load(mapping("prompt inspection lookup", map[string]any{
"profiles": map[string]any{"partial": map[string]any{}},
"prompts": map[string]any{
"multi": map[string]any{"v1": makePrompt("first"), "v2": makePrompt("second")},
"single": map[string]any{"v1": makePrompt("only")},
"selected": selectedPrompt,
},
}), nil)
if err != nil {
t.Fatal(err)
}
for _, selection := range []struct {
id, version string
}{
{id: "multi", version: "v2"},
{id: "single"},
} {
inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version)
{
inspection, err := state.GetPromptInspection(context.Background(), "selected", "")
if err != nil {
t.Fatalf("inspect %q/%q: %v", selection.id, selection.version, err)
t.Fatalf("inspect prompt ID: %v", err)
}
definition, hash, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version)
definition, hash, err := state.GetPromptSnapshot(context.Background(), "selected", "")
if err != nil {
t.Fatalf("snapshot %q/%q: %v", selection.id, selection.version, err)
t.Fatalf("snapshot prompt ID: %v", err)
}
if inspection.PromptID != definition.ID || inspection.PromptVersion != definition.Version ||
inspection.PromptHash != hash || inspection.DefaultProfileID != "partial" ||
@@ -184,53 +180,42 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing.
}
}
owned, err := state.GetPromptInspection(context.Background(), "single", "")
owned, err := state.GetPromptInspection(context.Background(), "selected", "")
if err != nil {
t.Fatal(err)
}
owned.Inputs[0].Name = "caller mutation"
owned.Inputs[0].Description = "caller mutation"
again, err := state.GetPromptInspection(context.Background(), "single", "v1")
again, err := state.GetPromptInspection(context.Background(), "selected", "")
if err != nil || again.Inputs[0].Name != "document" || again.Inputs[0].Description != "source document" {
t.Fatalf("inspection mutation escaped into captured state: inspection=%+v err=%v", again, err)
}
for _, selection := range []struct {
id, version string
}{
{id: "missing", version: "v1"},
{id: "multi", version: "V2"},
{id: " ", version: "v1"},
} {
inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version)
for _, id := range []string{"missing", " "} {
inspection, err := state.GetPromptInspection(context.Background(), id, "")
if inspection != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
t.Errorf("inspection selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, inspection, err)
t.Errorf("inspection lookup %q=(%+v, %v), want not found", id, inspection, err)
}
definition, _, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version)
definition, _, err := state.GetPromptSnapshot(context.Background(), id, "")
if definition != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
t.Errorf("snapshot selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, definition, err)
t.Errorf("snapshot lookup %q=(%+v, %v), want not found", id, definition, err)
}
}
if _, err := state.GetPromptInspection(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) {
t.Errorf("ambiguous inspection selection error=%v", err)
}
if _, _, err := state.GetPromptSnapshot(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) {
t.Errorf("ambiguous snapshot selection error=%v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err := state.GetPromptInspection(ctx, "single", "v1"); !errors.Is(err, context.Canceled) {
if _, err := state.GetPromptInspection(ctx, "selected", ""); !errors.Is(err, context.Canceled) {
t.Errorf("canceled inspection selection error=%v", err)
}
}
func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T) {
state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{"v1": map[string]any{
"inputs": []any{map[string]any{"name": " note ", "required": true}},
state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{
"version": "v1",
"inputs": []any{map[string]any{"name": " note ", "required": true}},
"messages": []any{map[string]any{"role": " USER ", "content": "", "content_file": nil,
"cache_control": map[string]any{"type": " ephemeral ", "ttl": "1h "}}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}}}), nil)
}}}), nil)
if err != nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) {
@@ -238,18 +223,19 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T
}
t.Fatal(err)
}
definition, _, err := state.prompt(" exact.id ", "v1")
definition, _, err := state.prompt(" exact.id ")
if err != nil || definition.ID != " exact.id " || definition.Templates[0].Content != "" || definition.Templates[0].Role != "user" ||
definition.Inputs[0].Name != "note" || definition.Templates[0].CacheControl.TTL != "1h" {
t.Fatalf("explicit empty content or literal ID was changed: %+v, %v", definition, err)
}
if _, _, err := state.prompt("exact.id", "v1"); !errors.Is(err, errPromptNotFound) {
if _, _, err := state.prompt("exact.id"); !errors.Is(err, errPromptNotFound) {
t.Fatalf("trimmed prompt identity unexpectedly matched: %v", err)
}
_, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
"id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}},
_, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{
"version": "v1",
"id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}}}), nil)
}}}), nil)
var loadError *configkit.LoadError
if !errors.As(err, &loadError) {
t.Fatalf("obsolete identity field should fail strict decoding: %v", err)
@@ -262,7 +248,8 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T
func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) {
state, err := load(mapping("requirement-metadata", map[string]any{
"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
"prompts": map[string]any{"example": map[string]any{
"version": "v1",
"session_id": "{{.session}}",
"inputs": []any{
map[string]any{"name": "absent"},
@@ -272,7 +259,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
},
"messages": []any{map[string]any{"role": "user", "content": "{{input \"absent\"}}"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}},
}},
}), nil)
if err != nil {
var loadError *configkit.LoadError
@@ -282,7 +269,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
t.Fatal(err)
}
snapshot, ok := state.GetRequirementSnapshot("example", "v1")
snapshot, ok := state.GetRequirementSnapshot("example", "")
if !ok || len(snapshot.Inputs) != 4 || len(snapshot.Templates.Messages) != 1 || snapshot.Templates.Session == nil {
t.Fatalf("requirement snapshot was not captured: %+v, %t", snapshot, ok)
}
@@ -300,7 +287,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
t.Errorf("snapshot did not use normalized final metadata: %+v", snapshot.Inputs[3])
}
definition, inspection, err := state.prompt("example", "v1")
definition, inspection, err := state.prompt("example")
if err != nil {
t.Fatal(err)
}
@@ -312,13 +299,10 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
*snapshot.Inputs[2].ContentType = "caller mutation"
snapshot.Inputs[3].Name = "caller mutation"
snapshot.Templates.Messages[0] = nil
again, ok := state.GetRequirementSnapshot("example", "v1")
again, ok := state.GetRequirementSnapshot("example", "")
if !ok || *again.Inputs[2].ContentType != "" || again.Inputs[3].Name != "normalized" || again.Templates.Messages[0] == nil {
t.Fatalf("snapshot mutation escaped into captured state: %+v, %t", again, ok)
}
if _, ok := state.GetRequirementSnapshot("example", "V1"); ok {
t.Fatal("snapshot lookup unexpectedly changed exact version matching")
}
}
func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) {
@@ -338,11 +322,12 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
input[name] = value
}
state, err := load(mapping("hash-"+variant.name, map[string]any{
"prompts": map[string]any{"same": map[string]any{"v1": map[string]any{
"prompts": map[string]any{"same": map[string]any{
"version": "v1",
"inputs": []any{input},
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}},
}},
}), nil)
if err != nil {
var loadError *configkit.LoadError
@@ -351,7 +336,7 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
}
t.Fatal(err)
}
_, inspection, err := state.prompt("same", "v1")
_, inspection, err := state.prompt("same")
if err != nil {
t.Fatal(err)
}
@@ -368,10 +353,10 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
}
func promptWithFile(name string) map[string]any {
return map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
return map[string]any{"prompts": map[string]any{"example": map[string]any{
"messages": []any{map[string]any{"role": "user", "content_file": name}},
"output": map[string]any{"format": "text", "validation_mode": "basic"},
}}}}
}}}
}
type countingFS struct {

View File

@@ -356,22 +356,20 @@ func smokeSelectionConfig() map[string]any {
},
"prompts": map[string]any{
"selection.prompt": map[string]any{
"v1": map[string]any{
"default_profile": "prompt-profile",
"inputs": []any{map[string]any{"name": "required_input", "required": true}},
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}},
"session_id": "{{.vars.session}}",
"output": map[string]any{
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
},
"version": "v1",
"default_profile": "prompt-profile",
"inputs": []any{map[string]any{"name": "required_input", "required": true}},
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}},
"session_id": "{{.vars.session}}",
"output": map[string]any{
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
},
},
"backend.prompt": map[string]any{
"v1": map[string]any{
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
"output": map[string]any{
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
},
"version": "v1",
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
"output": map[string]any{
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
},
},
},