190 lines
7.7 KiB
Go
190 lines
7.7 KiB
Go
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 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)
|
|
}
|
|
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 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()
|
|
}
|