Correct engine construction edge cases

This commit is contained in:
2026-08-11 21:54:47 +00:00
parent 57f2ce1ce4
commit 58ac3ce298
7 changed files with 161 additions and 22 deletions

View File

@@ -461,21 +461,20 @@ func newProfileRepository(profileDir string, options engineOptions) profile.Repo
}
func fileSource(name string) (fs.FS, string, error) {
cleanName := strings.TrimSpace(name)
if cleanName == "" {
if strings.TrimSpace(name) == "" {
return nil, "", ErrInvalidConfig
}
dir := filepath.Dir(cleanName)
base := filepath.Base(cleanName)
if base == "." || base == string(filepath.Separator) || strings.TrimSpace(base) == "" {
dir := filepath.Dir(name)
base := filepath.Base(name)
if base == "." || base == string(filepath.Separator) {
return nil, "", ErrInvalidConfig
}
info, err := os.Stat(cleanName)
info, err := os.Stat(name)
if err != nil {
return nil, "", fmt.Errorf("%w: failed to access source file %q: %v", ErrInvalidConfig, cleanName, err)
return nil, "", fmt.Errorf("%w: failed to access source file %q: %v", ErrInvalidConfig, name, err)
}
if info.IsDir() {
return nil, "", fmt.Errorf("%w: source path %q must be a file", ErrInvalidConfig, cleanName)
return nil, "", fmt.Errorf("%w: source path %q must be a file", ErrInvalidConfig, name)
}
return os.DirFS(dir), filepath.ToSlash(base), nil
}

View File

@@ -2374,6 +2374,116 @@ func TestRunStructuredOutputWorksWithSchemaFile(t *testing.T) {
}
}
func TestSingleFileOptionsPreserveWhitespaceInPaths(t *testing.T) {
profile := promptkit.Profile{
ID: "exact-path-profile",
Endpoint: "http://example.test/v1",
Model: "exact-path-model",
}
promptDocument := []byte(`
id: exact-path-prompt
version: "1.0.0"
default_profile: exact-path-profile
messages:
- role: user
content: exact path
output:
format: text
validation_mode: none
`)
profileDocument := []byte(`
id: exact-path-profile
endpoint: http://example.test/v1
model: exact-path-model
`)
tests := []struct {
name string
baseName string
content []byte
engine func(string) (*promptkit.Engine, error)
}{
{
name: "prompt",
baseName: "prompt.yaml",
content: promptDocument,
engine: func(path string) (*promptkit.Engine, error) {
return promptkit.NewEngine(promptkit.Config{},
promptkit.WithPromptFile(path),
promptkit.WithProfiles(profile),
)
},
},
{
name: "profile",
baseName: "profile.yaml",
content: profileDocument,
engine: func(path string) (*promptkit.Engine, error) {
return promptkit.NewEngine(promptkit.Config{},
promptkit.WithPromptFS(contractPromptFS("exact-path-prompt", "exact-path-profile", "exact path"), "."),
promptkit.WithProfileFile(path),
)
},
},
{
name: "schema",
baseName: "schema.json",
content: []byte(publicSchemaJSON()),
engine: func(path string) (*promptkit.Engine, error) {
promptSource := fstest.MapFS{
"prompt.yaml": &fstest.MapFile{Data: []byte(fmt.Sprintf(`
id: exact-path-prompt
version: "1.0.0"
default_profile: exact-path-profile
messages:
- role: user
content: exact path
output:
format: json
validation_mode: json_schema
schema_path: %q
`, filepath.Base(path)))},
}
return promptkit.NewEngine(promptkit.Config{},
promptkit.WithPromptFS(promptSource, "."),
promptkit.WithProfiles(profile),
promptkit.WithSchemaFile(path),
)
},
},
}
positions := []struct {
name string
prefix string
suffix string
}{
{name: "leading whitespace", prefix: " "},
{name: "trailing whitespace", suffix: " "},
}
for _, tc := range tests {
for _, position := range positions {
t.Run(tc.name+"/"+position.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), position.prefix+tc.baseName+position.suffix)
if err := os.WriteFile(path, tc.content, 0o644); err != nil {
t.Fatalf("write source file: %v", err)
}
engine, err := tc.engine(path)
if err != nil {
t.Fatalf("construct engine from exact path %q: %v", path, err)
}
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "exact-path-prompt"})
if err != nil {
t.Fatalf("prepare from exact path %q: %v", path, err)
}
if prepared.EffectiveModelParams.Model != "exact-path-model" {
t.Fatalf("prepared model = %q, want exact-path-model", prepared.EffectiveModelParams.Model)
}
})
}
}
}
func TestSourceOptionsRejectInvalidInputs(t *testing.T) {
missingFile := filepath.Join(t.TempDir(), "missing.yaml")
directoryPath := t.TempDir()
@@ -2405,6 +2515,16 @@ func TestSourceOptionsRejectInvalidInputs(t *testing.T) {
}
})
}
t.Run("later valid replacement does not hide invalid option", func(t *testing.T) {
_, err := promptkit.NewEngine(promptkit.Config{PromptDir: frameworkPromptDir},
promptkit.WithProfileFS(nil, "profiles"),
promptkit.WithProfileFS(contractProfileFS("profile", "model"), "."),
)
if !errors.Is(err, promptkit.ErrInvalidConfig) {
t.Fatalf("expected ErrInvalidConfig, got %v", err)
}
})
}
func TestPackageOptionsComposeFromSlice(t *testing.T) {

View File

@@ -15,10 +15,7 @@ const (
OpenAIChatCompletionsPath = "/chat/completions"
ExecutionDefaultTimeoutSeconds = 600
)
var (
LLMRequestTimeoutDefault = 10 * time.Minute
LLMRequestTimeoutDefault = 10 * time.Minute
)
func ExecutionTargetDefault() domain.ExecutionTarget {

View File

@@ -36,7 +36,8 @@ func FindYAMLFiles(ctx context.Context, root string) ([]string, error) {
return files, err
}
// FindFSYAMLFiles returns sorted paths for .yaml and .yml files under root in fsys.
// FindFSYAMLFiles returns root itself when it names a file. For a directory
// root, it returns sorted paths for .yaml and .yml files beneath that root.
func FindFSYAMLFiles(ctx context.Context, fsys fs.FS, root string) ([]string, error) {
cleanRoot := CleanFSRoot(root)
var files []string
@@ -52,6 +53,10 @@ func FindFSYAMLFiles(ctx context.Context, fsys fs.FS, root string) ([]string, er
if d.IsDir() {
return nil
}
if name == cleanRoot {
files = append(files, name)
return nil
}
if !IsYAMLFile(d.Name()) {
return nil
}
@@ -71,10 +76,10 @@ func RelativePath(root string, filePath string) string {
return filepath.Clean(rel)
}
// CleanFSRoot normalizes a root path for use with fs.FS.
// CleanFSRoot normalizes a root path for use with fs.FS while preserving
// nonblank leading and trailing whitespace.
func CleanFSRoot(root string) string {
root = strings.TrimSpace(root)
if root == "" || root == "." {
if strings.TrimSpace(root) == "" || root == "." {
return "."
}
return path.Clean(root)

View File

@@ -54,7 +54,7 @@ func TestFindFSYAMLFilesNestedSortedAndFiltered(t *testing.T) {
"other/ignored.yaml": &fstest.MapFile{Data: []byte("id: ignored")},
}
got, err := FindFSYAMLFiles(context.Background(), fsys, " prompts ")
got, err := FindFSYAMLFiles(context.Background(), fsys, "prompts")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
@@ -98,8 +98,11 @@ func TestCleanFSRoot(t *testing.T) {
want string
}{
{name: "empty", root: "", want: "."},
{name: "whitespace only", root: " \t ", want: "."},
{name: "dot", root: ".", want: "."},
{name: "trimmed", root: " prompts/../profiles ", want: "profiles"},
{name: "cleaned", root: "prompts/../profiles", want: "profiles"},
{name: "leading whitespace preserved", root: " profiles", want: " profiles"},
{name: "trailing whitespace preserved", root: "profiles ", want: "profiles "},
}
for _, tc := range tests {

View File

@@ -392,7 +392,7 @@ func (v *FSValidator) resolveSchemaPath(schemaPath string) (string, error) {
if err != nil {
return "", err
}
if cleanSchemaPath != path.Base(cleanRoot) {
if cleanSchemaPath != strings.TrimSpace(path.Base(cleanRoot)) {
return "", fmt.Errorf("schema path %q does not match schema file %q", cleanSchemaPath, path.Base(cleanRoot))
}
resolved = cleanRoot

View File

@@ -992,9 +992,9 @@ func TestFallbackProfileSourcePrecedence(t *testing.T) {
t.Run("in-memory profiles override ordinary and fallback profiles", func(t *testing.T) {
engine, err := promptkit.NewEngine(promptkit.Config{},
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
promptkit.WithProfileFS(contractProfileFS(profileID, "ordinary-model"), "."),
promptkit.WithProfiles(promptkit.Profile{ID: profileID, Endpoint: "http://example.test/v1", Model: "memory-model"}),
promptkit.WithProfileFS(contractProfileFS(profileID, "ordinary-model"), "."),
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
)
if err != nil {
t.Fatalf("construct engine: %v", err)
@@ -1007,8 +1007,8 @@ func TestFallbackProfileSourcePrecedence(t *testing.T) {
t.Run("ordinary filesystem source overrides fallback profile", func(t *testing.T) {
engine, err := promptkit.NewEngine(promptkit.Config{},
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
promptkit.WithProfileFS(contractProfileFS(profileID, "ordinary-model"), "."),
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
)
if err != nil {
t.Fatalf("construct engine: %v", err)
@@ -1018,6 +1018,21 @@ func TestFallbackProfileSourcePrecedence(t *testing.T) {
}
})
t.Run("ordinary option replaces configured directory", func(t *testing.T) {
profileDir := t.TempDir()
writePublicProfileFile(t, profileDir, profileID, "http://example.test/v1", "directory-model")
engine, err := promptkit.NewEngine(promptkit.Config{ProfileDir: profileDir},
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
promptkit.WithProfileFS(contractProfileFS(profileID, "option-model"), "."),
)
if err != nil {
t.Fatalf("construct engine: %v", err)
}
if model := prepareModel(t, engine, "prompt"); model != "option-model" {
t.Fatalf("expected ordinary option profile, got %q", model)
}
})
t.Run("configured directory overrides fallback profile", func(t *testing.T) {
profileDir := t.TempDir()
writePublicProfileFile(t, profileDir, profileID, "http://example.test/v1", "directory-model")