Contain prompt content paths within source roots
This commit is contained in:
254
engine_test.go
254
engine_test.go
@@ -1538,127 +1538,189 @@ unexpected: true
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareWorksWithPromptFSAndRelativeContentFile(t *testing.T) {
|
||||
promptFS := fstest.MapFS{
|
||||
"assets/prompts/fs-summary.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: fs.summary
|
||||
func TestPromptContentFilePathsAcrossSources(t *testing.T) {
|
||||
type pathCase struct {
|
||||
name string
|
||||
directoryPath string
|
||||
singleFilePath string
|
||||
directoryTarget string
|
||||
singleTarget string
|
||||
absolute bool
|
||||
symlink bool
|
||||
wantErr string
|
||||
}
|
||||
tests := []pathCase{
|
||||
{name: "ordinary sibling", directoryPath: "sibling.tmpl", singleFilePath: "sibling.tmpl", directoryTarget: "nested/sibling.tmpl", singleTarget: "sibling.tmpl"},
|
||||
{name: "parent remains inside root", directoryPath: "../shared.tmpl", singleFilePath: "nested/../shared.tmpl", directoryTarget: "shared.tmpl", singleTarget: "shared.tmpl"},
|
||||
{name: "parent escapes root", directoryPath: "../../outside.tmpl", singleFilePath: "../outside.tmpl", wantErr: "escapes source root"},
|
||||
{name: "absolute path", absolute: true, wantErr: "must be relative"},
|
||||
{name: "symlink escapes root", directoryPath: "escape.tmpl", singleFilePath: "escape.tmpl", symlink: true, wantErr: "escapes source root"},
|
||||
{name: "leading whitespace preserved", directoryPath: " body.tmpl", singleFilePath: " body.tmpl", directoryTarget: "nested/ body.tmpl", singleTarget: " body.tmpl"},
|
||||
{name: "trailing whitespace preserved", directoryPath: "body.tmpl ", singleFilePath: "body.tmpl ", directoryTarget: "nested/body.tmpl ", singleTarget: "body.tmpl "},
|
||||
}
|
||||
sources := []struct {
|
||||
name string
|
||||
singleFile bool
|
||||
injectedFS bool
|
||||
supportsSymlink bool
|
||||
}{
|
||||
{name: "operating system directory", supportsSymlink: true},
|
||||
{name: "injected filesystem", injectedFS: true},
|
||||
{name: "single file", singleFile: true, supportsSymlink: true},
|
||||
}
|
||||
|
||||
profile := promptkit.Profile{ID: "content-profile", Endpoint: "http://example.test/v1", Model: "content-model"}
|
||||
const promptID = "content-path-prompt"
|
||||
const body = "Exact content body."
|
||||
|
||||
for _, source := range sources {
|
||||
for _, tc := range tests {
|
||||
t.Run(source.name+"/"+tc.name, func(t *testing.T) {
|
||||
if tc.symlink && !source.supportsSymlink {
|
||||
t.Skip("source does not expose operating-system symlink semantics")
|
||||
}
|
||||
|
||||
workspace := t.TempDir()
|
||||
sourceRoot := filepath.Join(workspace, "prompts")
|
||||
outsidePath := filepath.Join(workspace, "outside.tmpl")
|
||||
if err := os.MkdirAll(filepath.Join(sourceRoot, "nested"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(outsidePath, []byte("Outside root."), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
contentFile := tc.directoryPath
|
||||
target := tc.directoryTarget
|
||||
promptRelativePath := "nested/prompt.yaml"
|
||||
if source.singleFile {
|
||||
contentFile = tc.singleFilePath
|
||||
target = tc.singleTarget
|
||||
promptRelativePath = "prompt.yaml"
|
||||
}
|
||||
if tc.absolute {
|
||||
contentFile = outsidePath
|
||||
if source.injectedFS {
|
||||
contentFile = "/outside.tmpl"
|
||||
}
|
||||
}
|
||||
|
||||
promptDocument := []byte(fmt.Sprintf(`
|
||||
id: %s
|
||||
version: "1.0.0"
|
||||
default_profile: contract-fast
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
default_profile: content-profile
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./messages/summary.tmpl
|
||||
content_file: %q
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"assets/prompts/messages/summary.tmpl": &fstest.MapFile{Data: []byte(`Summarize {{input "transcript"}} from prompt fs.`)},
|
||||
}
|
||||
`, promptID, contentFile))
|
||||
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||
PromptDir: t.TempDir(),
|
||||
ProfileDir: frameworkProfileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
}, promptkit.WithPromptFS(promptFS, "assets/prompts"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected engine construction to succeed, got %v", err)
|
||||
}
|
||||
var engine *promptkit.Engine
|
||||
var err error
|
||||
if source.injectedFS {
|
||||
promptFS := fstest.MapFS{
|
||||
"prompts/nested/prompt.yaml": &fstest.MapFile{Data: promptDocument},
|
||||
"outside.tmpl": &fstest.MapFile{Data: []byte("Outside root.")},
|
||||
}
|
||||
if target != "" {
|
||||
promptFS["prompts/"+filepath.ToSlash(target)] = &fstest.MapFile{Data: []byte(body)}
|
||||
}
|
||||
engine, err = promptkit.NewEngine(promptkit.Config{}, promptkit.WithPromptFS(promptFS, "prompts"), promptkit.WithProfiles(profile))
|
||||
} else {
|
||||
promptPath := filepath.Join(sourceRoot, filepath.FromSlash(promptRelativePath))
|
||||
if err := os.WriteFile(promptPath, promptDocument, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if target != "" {
|
||||
targetPath := filepath.Join(sourceRoot, filepath.FromSlash(target))
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(targetPath, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if tc.symlink {
|
||||
linkPath := filepath.Join(filepath.Dir(promptPath), "escape.tmpl")
|
||||
if err := os.Symlink(outsidePath, linkPath); err != nil {
|
||||
t.Skipf("symlinks are not supported: %v", err)
|
||||
}
|
||||
}
|
||||
if source.singleFile {
|
||||
engine, err = promptkit.NewEngine(promptkit.Config{}, promptkit.WithPromptFile(promptPath), promptkit.WithProfiles(profile))
|
||||
} else {
|
||||
engine, err = promptkit.NewEngine(promptkit.Config{PromptDir: sourceRoot}, promptkit.WithProfiles(profile))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("construct engine: %v", err)
|
||||
}
|
||||
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||
PromptID: "fs.summary",
|
||||
Inputs: map[string]promptkit.ArtifactRef{
|
||||
"transcript": promptkit.Inline("Rin opens the gate."),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||
}
|
||||
if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "prompt fs") {
|
||||
t.Fatalf("expected content_file body from prompt fs, got %+v", prepared.Messages)
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: promptID})
|
||||
if tc.wantErr != "" {
|
||||
if !errors.Is(err, promptkit.ErrPromptLoad) {
|
||||
t.Fatalf("expected ErrPromptLoad, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected error to contain %q, got %v", tc.wantErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("prepare: %v", err)
|
||||
}
|
||||
if len(prepared.Messages) != 1 || prepared.Messages[0].Content != body {
|
||||
t.Fatalf("expected exact content body, got %+v", prepared.Messages)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareWithPromptFSRejectsEscapedContentFile(t *testing.T) {
|
||||
func TestPromptContentFileFailuresPreservePublicError(t *testing.T) {
|
||||
promptFS := fstest.MapFS{
|
||||
"assets/prompts/fs-escape.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: fs.escape
|
||||
"prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: rejected-content-path
|
||||
version: "1.0.0"
|
||||
default_profile: contract-fast
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ../outside.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"assets/outside.tmpl": &fstest.MapFile{Data: []byte(`Outside root.`)},
|
||||
"outside.tmpl": &fstest.MapFile{Data: []byte("Outside root.")},
|
||||
}
|
||||
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||
PromptDir: t.TempDir(),
|
||||
ProfileDir: frameworkProfileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
}, promptkit.WithPromptFS(promptFS, "assets/prompts"))
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{}, promptkit.WithPromptFS(promptFS, "prompts"))
|
||||
if err != nil {
|
||||
t.Fatalf("expected engine construction to succeed, got %v", err)
|
||||
t.Fatalf("construct engine: %v", err)
|
||||
}
|
||||
|
||||
_, err = engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "fs.escape"})
|
||||
if !errors.Is(err, promptkit.ErrPromptLoad) {
|
||||
t.Fatalf("expected ErrPromptLoad, got %v", err)
|
||||
operations := []struct {
|
||||
name string
|
||||
run func() error
|
||||
}{
|
||||
{name: "inspect prompt", run: func() error {
|
||||
_, err := engine.InspectPrompt(context.Background(), "rejected-content-path", "")
|
||||
return err
|
||||
}},
|
||||
{name: "prepare execution", run: func() error {
|
||||
_, err := engine.PrepareExecution(context.Background(), promptkit.RunRequest{PromptID: "rejected-content-path"})
|
||||
return err
|
||||
}},
|
||||
{name: "run", run: func() error {
|
||||
_, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "rejected-content-path"})
|
||||
return err
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareWorksWithPromptFile(t *testing.T) {
|
||||
promptDir := t.TempDir()
|
||||
promptPath := filepath.Join(promptDir, "single.yaml")
|
||||
if err := os.WriteFile(promptPath, []byte(`
|
||||
id: single.file.prompt
|
||||
version: "1.0.0"
|
||||
default_profile: contract-fast
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./single.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(promptDir, "single.tmpl"), []byte(`Summarize {{input "transcript"}} from file.`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||
ProfileDir: frameworkProfileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
}, promptkit.WithPromptFile(promptPath))
|
||||
if err != nil {
|
||||
t.Fatalf("expected engine construction to succeed, got %v", err)
|
||||
}
|
||||
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||
PromptID: "single.file.prompt",
|
||||
Inputs: map[string]promptkit.ArtifactRef{
|
||||
"transcript": promptkit.Inline("Rin opens the gate."),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||
}
|
||||
if prepared.PromptID != "single.file.prompt" {
|
||||
t.Fatalf("unexpected prompt id: %q", prepared.PromptID)
|
||||
}
|
||||
if len(prepared.Messages) != 1 || !strings.Contains(prepared.Messages[0].Content, "from file") {
|
||||
t.Fatalf("expected content_file body from prompt file, got %+v", prepared.Messages)
|
||||
for _, operation := range operations {
|
||||
t.Run(operation.name, func(t *testing.T) {
|
||||
if err := operation.run(); !errors.Is(err, promptkit.ErrPromptLoad) {
|
||||
t.Fatalf("expected ErrPromptLoad, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user