package configuration import ( "context" "errors" "io/fs" "os" "path/filepath" "strings" "testing" "testing/fstest" "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit/internal/promptdef" ) func TestLoadCapturesInheritedImportedContent(t *testing.T) { files := fstest.MapFS{ "app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\n - path: ../other/definition.yaml\ncollections:\n docs:\n prompts:\n child:\n extends: base\n")}, "catalog/base.yaml": {Data: []byte("collections:\n docs:\n release_version: r1\n 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}}")}, "other/definition.yaml": {Data: []byte("collections:\n other:\n prompts:\n child:\n messages:\n - role: user\n content_file: ./body.md\n output:\n format: text\n validation_mode: none\n")}, "other/body.md": {Data: []byte("Other collection {{.name}}")}, } namespace := configkit.NewFilesystem("embedded", files) state, err := load(configkit.Input{ Source: namespace.File("app/main.yaml"), Imports: true, Filesystem: &configkit.FilesystemContext{FS: &namespace, Root: "."}, }, nil) if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) { t.Fatalf("%v: %+v", err, loadError.Diagnostics()) } t.Fatal(err) } definition, inspection, err := state.prompt("docs/child") if err != nil { t.Fatal(err) } if definition.Templates[0].Content != "Captured {{.name}}" || inspection.PromptHash == "" { t.Fatalf("inherited file content was not captured: %+v, %+v", definition, inspection) } message := definition.Templates[0] 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("collections", "docs", "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) } var originPath string for _, source := range state.result.Sources() { if source.ID == field.Origin.Locations[0].SourceID { originPath = source.Path } } if originPath != "catalog/base.yaml" { t.Fatalf("content origin = %q, want catalog/base.yaml", originPath) } other, otherInspection, err := state.prompt("other/child") if err != nil || other.Templates[0].Content != "Other collection {{.name}}" { t.Fatalf("other collection did not retain its source context: %+v, %v", other, err) } files["catalog/body.md"].Data = []byte("Changed") files["other/body.md"].Data = []byte("Changed other collection") again, againInspection, err := state.prompt("docs/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("docs/child") if err != nil || third.Templates[0].Content != "Captured {{.name}}" { t.Fatalf("caller mutation escaped into index: %+v, %v", third, err) } otherAgain, otherAgainInspection, err := state.prompt("other/child") if err != nil || otherAgain.Templates[0].Content != "Other collection {{.name}}" || otherAgainInspection.PromptHash != otherInspection.PromptHash { t.Fatalf("other captured definition changed after source mutation: %+v, %+v, %v", otherAgain, otherAgainInspection, err) } } func TestLoadUsesExplicitMappingContextAndClosesContent(t *testing.T) { filesystem := &countingFS{FS: fstest.MapFS{ "assets/body.md": {Data: []byte("From mapping")}, }} namespace := configkit.NewFilesystem("mapping-context", filesystem) source := configkit.Mapping("application", promptWithFile("body.md")) state, err := load(configkit.Input{ Source: source, Filesystem: &configkit.FilesystemContext{FS: &namespace, Root: ".", BaseDir: "assets"}, }, nil) if err != nil { t.Fatal(err) } 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/messages/0/content_file") { t.Fatalf("expected semantic path for contextless resource, got %v", err) } } 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 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 { t.Fatal(err) } state, err := load(configkit.Input{Source: configkit.File(definitionPath), Filesystem: &configkit.FilesystemContext{Root: root}}, nil) if err != nil { t.Fatal(err) } if err := os.WriteFile(contentPath, []byte("Modified"), 0o600); err != nil { t.Fatal(err) } 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 TestLoadRejectsMalformedStaticSyntaxAndCapturesOptionalVersion(t *testing.T) { _, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{ "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("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) } definition, inspection, err := state.prompt("example") if err != nil || definition.Version == nil || *definition.Version != "release label" || inspection.PromptVersion == nil || *inspection.PromptVersion != "release label" { t.Fatalf("optional version metadata was not retained: definition=%+v inspection=%+v err=%v", definition, inspection, err) } } func TestPromptVersionLabelPresenceAndOwnership(t *testing.T) { tests := []struct { name string version any include bool wantPresent bool wantValue string }{ {name: "absent"}, {name: "null", include: true}, {name: "empty", version: "", include: true, wantPresent: true}, {name: "exact text", version: " Release/v1 ", include: true, wantPresent: true, wantValue: " Release/v1 "}, } hashes := make(map[string]string, len(tests)) for _, test := range tests { t.Run(test.name, func(t *testing.T) { definition := map[string]any{ "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, } if test.include { definition["version"] = test.version } state, err := load(mapping("prompt version label", map[string]any{ "prompts": map[string]any{"example": definition}, }), nil) if err != nil { t.Fatal(err) } captured, inspection, err := state.prompt("example") if err != nil { t.Fatal(err) } identities, err := state.ListPrompts(context.Background()) if err != nil || len(identities) != 1 { t.Fatalf("list prompt labels: %#v, %v", identities, err) } check := func(name string, value *string) { t.Helper() if !test.wantPresent { if value != nil { t.Errorf("%s label = %q, want absent", name, *value) } return } if value == nil || *value != test.wantValue { t.Errorf("%s label = %v, want %q", name, value, test.wantValue) } } check("definition", captured.Version) check("inspection", inspection.PromptVersion) check("listing", identities[0].Version) if captured.Version != nil && (captured.Version == inspection.PromptVersion || captured.Version == identities[0].Version || inspection.PromptVersion == identities[0].Version) { t.Fatal("returned prompt labels share mutable pointers") } hashes[test.name] = inspection.PromptHash if captured.Version != nil { *captured.Version = "mutated definition" } if inspection.PromptVersion != nil { *inspection.PromptVersion = "mutated inspection" } if identities[0].Version != nil { *identities[0].Version = "mutated listing" } again, againInspection, err := state.prompt("example") if err != nil { t.Fatal(err) } check("captured definition after mutation", again.Version) check("captured inspection after mutation", againInspection.PromptVersion) }) } if hashes["absent"] == hashes["empty"] { t.Fatal("absent and declared-empty labels produced the same definition hash") } } func TestPromptVersionNullClearsInheritedLabel(t *testing.T) { prompt := map[string]any{ "version": "base-label", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, } state, err := load( mapping("clear inherited label", map[string]any{"prompts": map[string]any{"example": map[string]any{"version": nil}}}), []configkit.Underlay{{Inputs: []configkit.Input{mapping("base prompt", map[string]any{"prompts": map[string]any{"example": prompt}})}}}, ) if err != nil { t.Fatal(err) } definition, inspection, err := state.prompt("example") if err != nil || definition.Version != nil || inspection.PromptVersion != nil { t.Fatalf("null overlay retained inherited label: definition=%+v inspection=%+v err=%v", definition, inspection, err) } } func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing.T) { makePrompt := func(content string) map[string]any { return map[string]any{ "default_profile": "partial", "inputs": []any{map[string]any{ "name": "document", "required": true, "content_type": " text/plain ", "description": " source document ", }}, "messages": []any{map[string]any{"role": "user", "content": content}}, "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{ "selected": selectedPrompt, }, }), nil) if err != nil { t.Fatal(err) } { inspection, err := state.GetPromptInspection(context.Background(), "selected") if err != nil { t.Fatalf("inspect prompt ID: %v", err) } definition, hash, err := state.GetPromptSnapshot(context.Background(), "selected") if err != nil { t.Fatalf("snapshot prompt ID: %v", err) } if inspection.PromptID != definition.ID || inspection.PromptVersion == nil || definition.Version == nil || *inspection.PromptVersion != *definition.Version || inspection.PromptHash != hash || inspection.DefaultProfileID != "partial" || len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "document" || inspection.Inputs[0].ContentType != "text/plain" || inspection.Inputs[0].Description != "source document" { t.Fatalf("inspection did not match the selected snapshot: inspection=%+v definition=%+v hash=%q", inspection, definition, hash) } if len(definition.Inputs) != len(inspection.Inputs) || definition.Inputs[0].Name != inspection.Inputs[0].Name { t.Fatalf("definition and inspection inputs diverged: definition=%+v inspection=%+v", definition.Inputs, inspection.Inputs) } } 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(), "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 _, id := range []string{"missing", " "} { inspection, err := state.GetPromptInspection(context.Background(), id) if inspection != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) { t.Errorf("inspection lookup %q=(%+v, %v), want not found", id, inspection, err) } definition, _, err := state.GetPromptSnapshot(context.Background(), id) if definition != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) { t.Errorf("snapshot lookup %q=(%+v, %v), want not found", id, definition, err) } } ctx, cancel := context.WithCancel(context.Background()) cancel() 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{ "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) if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) { t.Fatalf("%v: %+v", err, loadError.Diagnostics()) } t.Fatal(err) } 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"); !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{ "version": "v1", "id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, }}}), nil) var loadError *configkit.LoadError if !errors.As(err, &loadError) { t.Fatalf("obsolete identity field should fail strict decoding: %v", err) } _, err = load(mapping("null-prompt", map[string]any{"prompts": map[string]any{"example": nil}}), nil) if err == nil { t.Fatal("null prompt collection was accepted") } } func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) { state, err := load(mapping("requirement-metadata", map[string]any{ "prompts": map[string]any{"example": map[string]any{ "version": "v1", "session_id": "{{.session}}", "inputs": []any{ map[string]any{"name": "absent"}, map[string]any{"name": "null", "content_type": nil, "description": nil}, map[string]any{"name": "empty", "content_type": "", "description": ""}, map[string]any{"name": "normalized", "content_type": " text/plain ", "description": " final text "}, }, "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 if errors.As(err, &loadError) { t.Fatalf("%v: %+v", err, loadError.Diagnostics()) } t.Fatal(err) } 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) } for _, index := range []int{0, 1} { if snapshot.Inputs[index].ContentType != nil || snapshot.Inputs[index].Description != nil { t.Errorf("input %q should not have explicit string metadata: %+v", snapshot.Inputs[index].Name, snapshot.Inputs[index]) } } if snapshot.Inputs[2].ContentType == nil || *snapshot.Inputs[2].ContentType != "" || snapshot.Inputs[2].Description == nil || *snapshot.Inputs[2].Description != "" { t.Errorf("explicit empty metadata presence was lost: %+v", snapshot.Inputs[2]) } if snapshot.Inputs[3].ContentType == nil || *snapshot.Inputs[3].ContentType != "text/plain" || snapshot.Inputs[3].Description == nil || *snapshot.Inputs[3].Description != "final text" { t.Errorf("snapshot did not use normalized final metadata: %+v", snapshot.Inputs[3]) } definition, inspection, err := state.prompt("example") if err != nil { t.Fatal(err) } if len(definition.Inputs) != 4 || len(inspection.Inputs) != 4 || inspection.Inputs[2].ContentType != "" || inspection.Inputs[2].Description != "" || inspection.Inputs[3].ContentType != "text/plain" || inspection.Inputs[3].Description != "final text" { t.Fatalf("existing prompt inspection shape or normalized values changed: %+v", inspection) } *snapshot.Inputs[2].ContentType = "caller mutation" snapshot.Inputs[3].Name = "caller mutation" snapshot.Templates.Messages[0] = nil 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) } } func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) { variants := []struct { name string fields map[string]any }{ {name: "absent"}, {name: "null", fields: map[string]any{"content_type": nil, "description": nil}}, {name: "empty", fields: map[string]any{"content_type": "", "description": ""}}, } var expectedHash string for _, variant := range variants { t.Run(variant.name, func(t *testing.T) { input := map[string]any{"name": "document"} for name, value := range variant.fields { input[name] = value } state, err := load(mapping("hash-"+variant.name, 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 if errors.As(err, &loadError) { t.Fatalf("%v: %+v", err, loadError.Diagnostics()) } t.Fatal(err) } _, inspection, err := state.prompt("same") if err != nil { t.Fatal(err) } if len(inspection.Inputs) != 1 || inspection.Inputs[0].ContentType != "" || inspection.Inputs[0].Description != "" { t.Fatalf("old inspection metadata changed: %+v", inspection.Inputs) } if expectedHash == "" { expectedHash = inspection.PromptHash } else if inspection.PromptHash != expectedHash { t.Fatalf("metadata presence changed the definition hash: got %q, want %q", inspection.PromptHash, expectedHash) } }) } } func promptWithFile(name string) 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 { fs.FS contentOpened int contentClosed int } func (filesystem *countingFS) Open(name string) (fs.File, error) { file, err := filesystem.FS.Open(name) if err != nil { return nil, err } if name == "assets/body.md" { filesystem.contentOpened++ return &countingFile{File: file, owner: filesystem}, nil } return file, nil } type countingFile struct { fs.File owner *countingFS } func (file *countingFile) Close() error { file.owner.contentClosed++ return file.File.Close() }