Add collection input helper
This commit is contained in:
@@ -8,9 +8,25 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ConfigKit"
|
"gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
|
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CollectionInput mounts source at the selected collection's configuration
|
||||||
|
// destination and enables ConfigKit imports for that input. The source is not
|
||||||
|
// read or validated until ConfigKit loads it. Callers may adjust the returned
|
||||||
|
// input, including its filesystem context or imports setting.
|
||||||
|
func CollectionInput(namespace string, source configkit.Source) (configkit.Input, error) {
|
||||||
|
if !domain.IsValidResourceNamespace(namespace) {
|
||||||
|
return configkit.Input{}, fmt.Errorf("%w: invalid collection namespace %q", ErrInvalidConfig, namespace)
|
||||||
|
}
|
||||||
|
return configkit.Input{
|
||||||
|
Source: source,
|
||||||
|
Destination: configkit.Keys("collections", namespace),
|
||||||
|
Imports: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DirectoryInputs selects the YAML files beneath an explicitly chosen host
|
// DirectoryInputs selects the YAML files beneath an explicitly chosen host
|
||||||
// directory as one strict-composition group. The returned order is deterministic;
|
// directory as one strict-composition group. The returned order is deterministic;
|
||||||
// it does not assign precedence among those files. Discovery is recursive over
|
// it does not assign precedence among those files. Discovery is recursive over
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package promptkit_test
|
package promptkit_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -16,6 +18,78 @@ type sourceDocument struct {
|
|||||||
Second string `yaml:"second"`
|
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) {
|
func TestDirectoryInputsSelectsSortedFilesWithRootContext(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {
|
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {
|
||||||
|
|||||||
@@ -593,6 +593,13 @@ Validation: source-helper contract tests and focused configuration/root tests.
|
|||||||
Verify helper construction itself performs no source read using an existing
|
Verify helper construction itself performs no source read using an existing
|
||||||
controlled source seam where needed, rather than adding a production interface.
|
controlled source seam where needed, rather than adding a production interface.
|
||||||
|
|
||||||
|
Completion: added `CollectionInput` with shared namespace validation, a
|
||||||
|
`collections/<namespace>` destination, and imports enabled without reading the
|
||||||
|
source. Contract tests cover invalid namespaces and deferred source loading;
|
||||||
|
native-file and embedded-filesystem imports assemble successfully, preserve
|
||||||
|
their source context, and accept a later override. Validation passed:
|
||||||
|
`GOWORK=off go test ./...`, `GOWORK=off go vet .`, and `git diff --check`.
|
||||||
|
|
||||||
## Stage 12: Add Immutable Scopes To Ordinary Execution
|
## Stage 12: Add Immutable Scopes To Ordinary Execution
|
||||||
|
|
||||||
Scope: public `Namespaces`/`WithNamespaces` and shared engine-boundary routing.
|
Scope: public `Namespaces`/`WithNamespaces` and shared engine-boundary routing.
|
||||||
|
|||||||
Reference in New Issue
Block a user