package configuration import ( "errors" "io/fs" "os" "path/filepath" "strings" "testing" "testing/fstest" "gitea.maximumdirect.net/eric/ConfigKit" ) 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/body.md": {Data: []byte("Captured {{.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("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("prompts", "child", "v1", "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) } files["catalog/body.md"].Data = []byte("Changed") again, againInspection, err := state.prompt("child", "v1") 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") if err != nil || third.Templates[0].Content != "Captured {{.name}}" { t.Fatalf("caller mutation escaped into index: %+v, %v", third, 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", "v1") 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") { 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 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", "v1") 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) { _, 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"}}, }}}), 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"}}, }}}), 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) } } 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}}, "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 ", "v1") 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) { 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"}}, "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{"v1": map[string]any{ "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", "v1") 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", "v1") 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", "v1") 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) { 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{"v1": map[string]any{ "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", "v1") 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{"v1": 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() }