Store one prompt definition per ID
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user