159 lines
5.4 KiB
Go
159 lines
5.4 KiB
Go
package promptdef
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
|
)
|
|
|
|
func TestFilesystemRepository_GetPromptDefinition(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
if err := copyTree("testdata", tmpDir); err != nil {
|
|
t.Fatalf("failed to copy testdata: %v", err)
|
|
}
|
|
|
|
repo := NewFilesystemRepository(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
t.Run("valid inline prompt", func(t *testing.T) {
|
|
p, err := repo.GetPromptDefinition(ctx, "valid-inline", "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if p.ID != "valid-inline" {
|
|
t.Fatalf("unexpected id: %q", p.ID)
|
|
}
|
|
if p.Version != "1.0.0" {
|
|
t.Fatalf("unexpected version: %q", p.Version)
|
|
}
|
|
if p.OutputFormat != domain.FormatMarkdown {
|
|
t.Fatalf("unexpected output format: %q", p.OutputFormat)
|
|
}
|
|
if p.Validation.ValidationMode != domain.ValidationBasic {
|
|
t.Fatalf("unexpected validation mode: %q", p.Validation.ValidationMode)
|
|
}
|
|
if len(p.Templates) != 2 {
|
|
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
|
}
|
|
if len(p.Inputs) != 1 {
|
|
t.Fatalf("expected 1 input, got %d", len(p.Inputs))
|
|
}
|
|
if p.Inputs[0].ContentType != "text/markdown" {
|
|
t.Fatalf("expected input content_type to be preserved, got %q", p.Inputs[0].ContentType)
|
|
}
|
|
})
|
|
|
|
t.Run("valid file-backed prompt", func(t *testing.T) {
|
|
p, err := repo.GetPromptDefinition(ctx, "valid-file-backed", "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(p.Templates) != 2 {
|
|
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
|
}
|
|
if !strings.Contains(p.Templates[1].Content, "{{input \"transcript\"}}") {
|
|
t.Fatalf("expected content_file template body to be loaded, got %q", p.Templates[1].Content)
|
|
}
|
|
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)
|
|
}
|
|
})
|
|
|
|
t.Run("prompt with default_profile", func(t *testing.T) {
|
|
p, err := repo.GetPromptDefinition(ctx, "with-default-profile", "")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if p.DefaultProfile != "local-default" {
|
|
t.Fatalf("unexpected default profile: %q", p.DefaultProfile)
|
|
}
|
|
if len(p.Inputs) != 1 {
|
|
t.Fatalf("expected one input, got %d", len(p.Inputs))
|
|
}
|
|
if p.Inputs[0].ContentType != "" {
|
|
t.Fatalf("expected missing content_type to remain empty, got %q", p.Inputs[0].ContentType)
|
|
}
|
|
})
|
|
|
|
t.Run("version lookup", func(t *testing.T) {
|
|
_, err := repo.GetPromptDefinition(ctx, "valid-inline", "9.9.9")
|
|
if !errors.Is(err, ErrPromptDefinitionNotFound) {
|
|
t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
cases := []struct {
|
|
name string
|
|
id string
|
|
targetErr error
|
|
errSubstrs []string
|
|
}{
|
|
{name: "invalid YAML", id: "invalid_yaml", targetErr: ErrInvalidYAML},
|
|
{name: "missing id", id: "missing_id", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"id is required"}},
|
|
{name: "no messages", id: "no_messages", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"at least one message is required"}},
|
|
{name: "both content and content_file", id: "both_content_and_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}},
|
|
{name: "neither content nor content_file", id: "neither_content_nor_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}},
|
|
{name: "missing content_file", id: "missing_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"failed to read content_file"}},
|
|
{name: "duplicate input names", id: "duplicate_input_names", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"duplicate input name"}},
|
|
{name: "invalid validation mode", id: "invalid_validation_mode", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"invalid validation mode"}},
|
|
{name: "json_schema without schema_path", id: "json_schema_without_schema_path", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"schema_path"}},
|
|
{name: "unknown input field", id: "unknown_input_field", targetErr: ErrInvalidYAML, errSubstrs: []string{"field unknown_input_setting not found"}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := repo.GetPromptDefinition(ctx, tc.id, "")
|
|
if !errors.Is(err, tc.targetErr) {
|
|
t.Fatalf("expected %v, got %v", tc.targetErr, err)
|
|
}
|
|
for _, sub := range tc.errSubstrs {
|
|
if !strings.Contains(err.Error(), sub) {
|
|
t.Fatalf("expected error to contain %q, got %v", sub, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("prompt definition not found", func(t *testing.T) {
|
|
_, err := repo.GetPromptDefinition(ctx, "does-not-exist", "")
|
|
if !errors.Is(err, ErrPromptDefinitionNotFound) {
|
|
t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func copyTree(src, dst string) error {
|
|
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
return nil
|
|
}
|
|
|
|
target := filepath.Join(dst, rel)
|
|
if d.IsDir() {
|
|
return os.MkdirAll(target, 0o755)
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(target, data, 0o644)
|
|
})
|
|
}
|