Unify prompt repository source handling
This commit is contained in:
@@ -3,6 +3,7 @@ package promptdef
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,13 +15,39 @@ import (
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
func TestFilesystemRepository_GetPromptDefinition(t *testing.T) {
|
||||
func TestPromptRepositoryDefinitionFixtures(t *testing.T) {
|
||||
sources := []struct {
|
||||
name string
|
||||
newRepository func(string) Repository
|
||||
contentPathsAreFull bool
|
||||
}{
|
||||
{
|
||||
name: "operating system",
|
||||
newRepository: NewFilesystemRepository,
|
||||
contentPathsAreFull: true,
|
||||
},
|
||||
{
|
||||
name: "filesystem",
|
||||
newRepository: func(root string) Repository {
|
||||
return NewFSRepository(os.DirFS(root), ".")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
testPromptRepositoryDefinitionFixtures(t, source.newRepository, source.contentPathsAreFull)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testPromptRepositoryDefinitionFixtures(t *testing.T, newRepository func(string) Repository, contentPathsAreFull bool) {
|
||||
tmpDir := t.TempDir()
|
||||
if err := copyTree("testdata", tmpDir); err != nil {
|
||||
t.Fatalf("failed to copy testdata: %v", err)
|
||||
}
|
||||
|
||||
repo := NewFilesystemRepository(tmpDir)
|
||||
repo := newRepository(tmpDir)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("valid inline prompt", func(t *testing.T) {
|
||||
@@ -65,8 +92,8 @@ func TestFilesystemRepository_GetPromptDefinition(t *testing.T) {
|
||||
if p.Templates[1].ContentFile == "" {
|
||||
t.Fatal("expected ContentFile source metadata to be preserved")
|
||||
}
|
||||
if !filepath.IsAbs(p.Templates[1].ContentFile) {
|
||||
t.Fatalf("expected resolved content_file path to be absolute, got %q", p.Templates[1].ContentFile)
|
||||
if filepath.IsAbs(p.Templates[1].ContentFile) != contentPathsAreFull {
|
||||
t.Fatalf("unexpected content_file path representation: %q", p.Templates[1].ContentFile)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -456,63 +483,6 @@ func (f *recordingFS) openCount(name string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func TestFSRepositoryRejectsDuplicatePromptIDs(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"one.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: duplicate-fs-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: First.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"nested/two.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: duplicate-fs-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: Second.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
}, ".")
|
||||
|
||||
_, err := repo.GetPromptDefinition(context.Background(), "duplicate-fs-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidPromptDefinition) {
|
||||
t.Fatalf("expected ErrInvalidPromptDefinition, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "one.yaml") || !strings.Contains(err.Error(), "nested/two.yaml") {
|
||||
t.Fatalf("expected duplicate paths in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSRepositoryRejectsUnknownYAMLFields(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"not_named_like_id.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: strict-fs-prompt
|
||||
version: "1.0.0"
|
||||
unknown: true
|
||||
messages:
|
||||
- role: user
|
||||
content: Invalid.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
}, ".")
|
||||
|
||||
_, err := repo.GetPromptDefinition(context.Background(), "strict-fs-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidYAML) {
|
||||
t.Fatalf("expected ErrInvalidYAML, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptRepositorySelectionUsesYAMLMetadata(t *testing.T) {
|
||||
const validDefinition = `
|
||||
id: selected-prompt
|
||||
@@ -529,6 +499,7 @@ output:
|
||||
files map[string]string
|
||||
wantErr error
|
||||
diagnostics []string
|
||||
wantContent string
|
||||
}{
|
||||
{
|
||||
name: "same-stem strict error with different YAML ID is unrelated",
|
||||
@@ -561,6 +532,41 @@ output:
|
||||
"valid.yaml": validDefinition,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "selected content is resolved relative to its definition",
|
||||
files: map[string]string{
|
||||
"nested/selected.yaml": `
|
||||
id: selected-prompt
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./content/selected.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`,
|
||||
"nested/content/selected.tmpl": "selected from file",
|
||||
},
|
||||
wantContent: "selected from file",
|
||||
},
|
||||
{
|
||||
name: "duplicate selected definitions are ambiguous",
|
||||
files: map[string]string{
|
||||
"selected-a.yaml": validDefinition,
|
||||
"nested/selected-b.yaml": `
|
||||
id: selected-prompt
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content: duplicate
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`,
|
||||
},
|
||||
wantErr: ErrInvalidPromptDefinition,
|
||||
diagnostics: []string{"duplicate prompt definition id", "selected-a.yaml", "nested/selected-b.yaml"},
|
||||
},
|
||||
{
|
||||
name: "selected strict error is authoritative",
|
||||
files: map[string]string{
|
||||
@@ -587,6 +593,23 @@ output:
|
||||
wantErr: ErrInvalidPromptDefinition,
|
||||
diagnostics: []string{"selected-invalid.yaml", "at least one message"},
|
||||
},
|
||||
{
|
||||
name: "selected content error includes definition context",
|
||||
files: map[string]string{
|
||||
"selected-missing-content.yaml": `
|
||||
id: selected-prompt
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: missing.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`,
|
||||
},
|
||||
wantErr: ErrInvalidPromptDefinition,
|
||||
diagnostics: []string{"selected-missing-content.yaml", "failed to read content_file", "missing.tmpl"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, source := range promptRepositorySources() {
|
||||
@@ -608,7 +631,11 @@ output:
|
||||
if err != nil {
|
||||
t.Fatalf("load selected prompt: %v", err)
|
||||
}
|
||||
if got.ID != "selected-prompt" || got.Version != "1" || got.Templates[0].Content != "selected" {
|
||||
wantContent := tc.wantContent
|
||||
if wantContent == "" {
|
||||
wantContent = "selected"
|
||||
}
|
||||
if got.ID != "selected-prompt" || got.Version != "1" || got.Templates[0].Content != wantContent {
|
||||
t.Fatalf("unexpected selected definition: %+v", got)
|
||||
}
|
||||
})
|
||||
@@ -616,6 +643,32 @@ output:
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptRepositoryHonorsCancellation(t *testing.T) {
|
||||
for _, source := range promptRepositorySources() {
|
||||
t.Run(source.name, func(t *testing.T) {
|
||||
repo := source.newRepository(t, map[string]string{
|
||||
"definition.yaml": `
|
||||
id: cancelled-prompt
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content: selected
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`,
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, err := repo.GetPromptDefinition(ctx, "cancelled-prompt", "1")
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("expected context cancellation, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptRepositoryRequiresOneYAMLDocument(t *testing.T) {
|
||||
const definition = `
|
||||
id: one-document
|
||||
@@ -902,6 +955,70 @@ func promptRepositorySources() []promptRepositorySource {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPromptRepositoryLookup(b *testing.B) {
|
||||
for _, size := range []int{10, 1000} {
|
||||
b.Run(fmt.Sprintf("catalog-%d", size), func(b *testing.B) {
|
||||
files := fstest.MapFS{
|
||||
"target.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: target
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: target.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`)},
|
||||
"target.tmpl": &fstest.MapFile{Data: []byte("selected")},
|
||||
}
|
||||
metadataNames := []string{"target.yaml"}
|
||||
contentNames := make([]string, 0, size-1)
|
||||
for i := 1; i < size; i++ {
|
||||
definitionName := fmt.Sprintf("prompt-%04d.yaml", i)
|
||||
contentName := fmt.Sprintf("prompt-%04d.tmpl", i)
|
||||
files[definitionName] = &fstest.MapFile{Data: []byte(fmt.Sprintf(`
|
||||
id: prompt-%04d
|
||||
version: "1"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: %s
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`, i, contentName))}
|
||||
files[contentName] = &fstest.MapFile{Data: []byte("unrelated")}
|
||||
metadataNames = append(metadataNames, definitionName)
|
||||
contentNames = append(contentNames, contentName)
|
||||
}
|
||||
|
||||
fsys := &recordingFS{FS: files}
|
||||
repo := NewFSRepository(fsys, ".")
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := repo.GetPromptDefinition(context.Background(), "target", "1"); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
if got := fsys.openCount("target.tmpl"); got != b.N {
|
||||
b.Fatalf("selected content opens = %d, want %d", got, b.N)
|
||||
}
|
||||
for _, name := range contentNames {
|
||||
if got := fsys.openCount(name); got != 0 {
|
||||
b.Fatalf("unrelated content %q opened %d times", name, got)
|
||||
}
|
||||
}
|
||||
for _, name := range metadataNames {
|
||||
if got := fsys.openCount(name); got != b.N {
|
||||
b.Fatalf("metadata %q opens = %d, want %d", name, got, b.N)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertCacheControl(t *testing.T, got *domain.CacheControl, wantType domain.CacheControlType, wantTTL string) {
|
||||
t.Helper()
|
||||
if got == nil {
|
||||
|
||||
Reference in New Issue
Block a user