142 lines
5.8 KiB
Go
142 lines
5.8 KiB
Go
|
|
package promptkit_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"reflect"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"go.maximumdirect.net/promptkit"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestContinuationBranchesAcrossEngines(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
firstClient := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "first answer"}}
|
||
|
|
firstEngine := newSimpleEngine(t, "first question", promptkit.WithLLMClient(firstClient))
|
||
|
|
initial, err := firstEngine.Run(ctx, promptkit.RunRequest{PromptID: "prompt", CaptureExchange: true, APIKey: "synthetic-first-key"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
original := initial.Exchange.Details()
|
||
|
|
secondClient := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "follow-up answer"}}
|
||
|
|
secondEngine := newNamespacedContractEngine(t, secondClient)
|
||
|
|
scoped, err := secondEngine.WithNamespaces(promptkit.Namespaces{Prompt: "alpha", Profile: "alpha"})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request := promptkit.RunRequest{PromptID: "shared", ProfileID: "shared", ContinueFrom: initial.Exchange, SessionID: "new-session", Validation: &promptkit.OutputContract{Format: promptkit.FormatJSON, ValidationMode: promptkit.ValidationJSON}, AppendedMessages: []promptkit.RenderedMessage{{Role: promptkit.RoleUser, Content: "branch one"}}}
|
||
|
|
branch, err := scoped.Run(ctx, request)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request.AppendedMessages[0].Content = "branch two"
|
||
|
|
sibling, err := scoped.Run(ctx, request)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
next, err := scoped.Run(ctx, promptkit.RunRequest{PromptID: "shared", ProfileID: "shared", ContinueFrom: branch.Exchange})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if branch.ParentRunID != initial.RunID || sibling.ParentRunID != initial.RunID || next.ParentRunID != branch.RunID || branch.PromptID != "alpha/shared" || branch.SelectedProfileID != "alpha/shared" || branch.SessionID != "new-session" {
|
||
|
|
t.Fatalf("ancestry or selections incorrect: %#v %#v %#v", branch, sibling, next)
|
||
|
|
}
|
||
|
|
if !reflect.DeepEqual(initial.Exchange.Details(), original) {
|
||
|
|
t.Fatal("branching changed source")
|
||
|
|
}
|
||
|
|
if branch.Validation.Status != promptkit.ValidationFailed || branch.ModelName != "alpha-model" {
|
||
|
|
t.Fatalf("follow-up settings: %#v", branch)
|
||
|
|
}
|
||
|
|
branchMessages := branch.Exchange.Details().Messages
|
||
|
|
siblingMessages := sibling.Exchange.Details().Messages
|
||
|
|
if len(branchMessages) != 5 || len(siblingMessages) != 5 || branchMessages[3].Content != "branch one" || siblingMessages[3].Content != "branch two" || len(next.Exchange.Details().Messages) != 7 {
|
||
|
|
t.Fatalf("branch histories = %#v / %#v", branchMessages, siblingMessages)
|
||
|
|
}
|
||
|
|
encoded, err := json.Marshal(branch.Exchange.Details())
|
||
|
|
if err != nil || !json.Valid(encoded) {
|
||
|
|
t.Fatalf("details JSON = %s, %v", encoded, err)
|
||
|
|
}
|
||
|
|
branchMessages[0].Content = "changed details"
|
||
|
|
branch.RawOutput = "changed result"
|
||
|
|
if initial.Exchange.Details().Messages[0].Content != "first question" || branch.Exchange.Details().Messages[4].Content != "follow-up answer" {
|
||
|
|
t.Fatal("caller mutation changed captured history")
|
||
|
|
}
|
||
|
|
for _, request := range secondClient.requests {
|
||
|
|
if request.APIKey == "synthetic-first-key" || request.Prompt.Messages[0].Content != "first question" || request.Prompt.Messages[1].Content != "first answer" {
|
||
|
|
t.Fatalf("unexpected follow-up request: %#v", request)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContinuationDoesNotReloadCapturedInputs(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
path := filepath.Join(t.TempDir(), "input.txt")
|
||
|
|
if err := os.WriteFile(path, []byte("original input"), 0600); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: ""}}
|
||
|
|
engine := newSimpleEngine(t, `{{input "source"}}`, promptkit.WithLLMClient(client))
|
||
|
|
initial, err := engine.Run(ctx, promptkit.RunRequest{PromptID: "prompt", Inputs: map[string]promptkit.ArtifactRef{"source": promptkit.File(path)}, CaptureExchange: true})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := os.WriteFile(path, []byte("new input"), 0600); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
request := promptkit.RunRequest{PromptID: "prompt", Inputs: map[string]promptkit.ArtifactRef{"source": promptkit.File(path)}, ContinueFrom: initial.Exchange}
|
||
|
|
prepared, err := engine.PrepareExecution(ctx, request)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := os.Remove(path); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
result, err := engine.RunPrepared(ctx, prepared)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
want := []promptkit.RenderedMessage{{Role: promptkit.RoleUser, Content: "original input"}, {Role: promptkit.RoleAssistant, Content: ""}, {Role: promptkit.RoleUser, Content: "new input"}, {Role: promptkit.RoleAssistant, Content: ""}}
|
||
|
|
if !reflect.DeepEqual(result.Exchange.Details().Messages, want) {
|
||
|
|
t.Fatalf("history = %#v", result.Exchange.Details().Messages)
|
||
|
|
}
|
||
|
|
if result, err := engine.Run(ctx, request); err == nil || result != nil {
|
||
|
|
t.Fatalf("missing new input = %#v, %v", result, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestContinuationSourceSupportsConcurrentRuns(t *testing.T) {
|
||
|
|
client := &preparedRecordingClient{response: &promptkit.GenerateResponse{Content: "answer"}}
|
||
|
|
engine := newSimpleEngine(t, "question", promptkit.WithLLMClient(client))
|
||
|
|
initial, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt", CaptureExchange: true})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
const count = 8
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
errors := make(chan error, count)
|
||
|
|
for i := 0; i < count; i++ {
|
||
|
|
wg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
result, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt", ContinueFrom: initial.Exchange})
|
||
|
|
if err == nil && (result.Exchange == nil || result.ParentRunID != initial.RunID) {
|
||
|
|
t.Error("missing continuation ancestry")
|
||
|
|
}
|
||
|
|
errors <- err
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
wg.Wait()
|
||
|
|
close(errors)
|
||
|
|
for err := range errors {
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(initial.Exchange.Details().Messages) != 2 {
|
||
|
|
t.Fatal("source changed")
|
||
|
|
}
|
||
|
|
}
|