package promptkit_test import ( "context" "errors" "io/fs" "os" "path/filepath" "testing" "testing/fstest" "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit" ) type sourceDocument struct { First string `yaml:"first"` Second string `yaml:"second"` } func TestCollectionInputValidatesWithoutReadingSource(t *testing.T) { input, err := promptkit.CollectionInput("docs", configkit.File(filepath.Join(t.TempDir(), "not-read.yaml"))) if err != nil { t.Fatal(err) } if input.Destination.String() != "/collections/docs" || !input.Imports { t.Fatalf("collection input = %+v, want destination /collections/docs with imports enabled", input) } for _, namespace := range []string{"", " ", "docs/api"} { if _, err := promptkit.CollectionInput(namespace, configkit.NoSource()); !errors.Is(err, promptkit.ErrInvalidConfig) { t.Errorf("namespace %q error = %v, want ErrInvalidConfig", namespace, err) } } } func TestCollectionInputLoadsNativeImportsAndLaterOverride(t *testing.T) { directory := t.TempDir() mainPath := filepath.Join(directory, "collection.yaml") defaultsPath := filepath.Join(directory, "defaults.yaml") if err := os.WriteFile(mainPath, []byte("configkit:\n imports:\n - path: defaults.yaml\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(defaultsPath, []byte("prompts:\n greeting:\n version: imported\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n"), 0o600); err != nil { t.Fatal(err) } input, err := promptkit.CollectionInput("docs", configkit.File(mainPath)) if err != nil { t.Fatal(err) } engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}, Main: &configkit.Input{ Source: configkit.Mapping("later collection override", map[string]any{"prompts": map[string]any{"greeting": map[string]any{"version": "overridden"}}}), Destination: configkit.Keys("collections", "docs"), }, }}) if err != nil { t.Fatalf("construct engine with native collection import: %v", err) } inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting") if err != nil || inspection.PromptVersion == nil || *inspection.PromptVersion != "overridden" { t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err) } } func TestCollectionInputLoadsEmbeddedImportsAndLaterOverride(t *testing.T) { files := fstest.MapFS{ "collection.yaml": {Data: []byte("configkit:\n imports:\n - path: defaults.yaml\n")}, "defaults.yaml": {Data: []byte("prompts:\n greeting:\n version: imported\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n")}, } filesystem := configkit.NewFilesystem("embedded collection", files) input, err := promptkit.CollectionInput("docs", filesystem.File("collection.yaml")) if err != nil { t.Fatal(err) } input.Filesystem = &configkit.FilesystemContext{FS: &filesystem, Root: "."} engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}, Main: &configkit.Input{ Source: configkit.Mapping("later collection override", map[string]any{"prompts": map[string]any{"greeting": map[string]any{"version": "overridden"}}}), Destination: configkit.Keys("collections", "docs"), }, }}) if err != nil { t.Fatalf("construct engine with embedded collection import: %v", err) } inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting") if err != nil || inspection.PromptVersion == nil || *inspection.PromptVersion != "overridden" { t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err) } } func TestDirectoryInputsSelectsSortedFilesWithRootContext(t *testing.T) { root := t.TempDir() if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil { t.Fatal(err) } for name, data := range map[string]string{ "a.yaml": "first: one\n", "nested/b.yml": "second: two\n", "nested/body.md": "message", } { if err := os.WriteFile(filepath.Join(root, name), []byte(data), 0o600); err != nil { t.Fatal(err) } } inputs, err := promptkit.DirectoryInputs(root) if err != nil || len(inputs) != 2 || inputs[0].Imports || inputs[1].Imports { t.Fatalf("directory inputs: %+v, %v", inputs, err) } assertSourceInputs(t, inputs, "message") if _, err := promptkit.DirectoryInputs(filepath.Join(root, "missing")); err == nil { t.Fatal("missing selected directory succeeded") } if empty, err := promptkit.DirectoryInputs(t.TempDir()); err != nil || len(empty) != 0 { t.Fatalf("empty directory: %+v, %v", empty, err) } } func TestFilesystemInputsSelectsEmbeddedFilesWithRootContext(t *testing.T) { files := fstest.MapFS{ "settings/a.yaml": {Data: []byte("first: one\n")}, "settings/nested/b.yml": {Data: []byte("second: two\n")}, "settings/nested/body.md": {Data: []byte("message")}, } inputs, err := promptkit.FilesystemInputs("embedded", files, "settings") if err != nil || len(inputs) != 2 || inputs[0].Imports || inputs[1].Imports { t.Fatalf("filesystem inputs: %+v, %v", inputs, err) } assertSourceInputs(t, inputs, "message") if _, err := promptkit.FilesystemInputs("embedded", files, "missing"); err == nil { t.Fatal("missing selected root succeeded") } } func assertSourceInputs(t *testing.T, inputs []configkit.Input, expected string) { t.Helper() result, err := configkit.LoadLayered(configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[sourceDocument]{ Underlays: []configkit.Underlay{{Inputs: inputs}}, }) if err != nil { t.Fatal(err) } if result.Config.First != "one" || result.Config.Second != "two" { t.Fatalf("configuration files did not compose: %+v", result.Config) } field, found := result.Metadata().Lookup(configkit.Keys("second")) if !found || len(field.Origin.Locations) != 1 { t.Fatalf("missing second-field origin: %+v", field) } context, found := result.SourceContext(field.Origin.Locations[0].SourceID) if !found || context.BaseDir != "nested" { t.Fatalf("unexpected rooted source context: %+v", context) } data, err := fs.ReadFile(context.FS, "nested/body.md") if err != nil || string(data) != expected { t.Fatalf("rooted resource read: %q, %v", data, err) } }