Characterize public engine execution behavior
This commit is contained in:
366
engine_test.go
366
engine_test.go
@@ -274,30 +274,183 @@ func TestGenerateRequestFormattingRedactsDirectAPIKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparePreservesExplicitZeroExecutionOverrides(t *testing.T) {
|
||||
engine := newContractEngine(t)
|
||||
zeroFloat := 0.0
|
||||
zeroInt := 0
|
||||
|
||||
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.File(frameworkTranscriptPath),
|
||||
"glossary": scriptorium.File(frameworkGlossaryPath),
|
||||
},
|
||||
Execution: &scriptorium.ExecutionTargetOverride{
|
||||
Temperature: &zeroFloat,
|
||||
MaxTokens: &zeroInt,
|
||||
TopP: &zeroFloat,
|
||||
TimeoutSeconds: &zeroInt,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||
func TestEngineExecutionSettingPrecedence(t *testing.T) {
|
||||
floatPointer := func(value float64) *float64 {
|
||||
return &value
|
||||
}
|
||||
target := prepared.EffectiveModelParams
|
||||
if target.Temperature != 0 || target.MaxTokens != 0 || target.TopP != 0 || target.TimeoutSeconds != 0 {
|
||||
t.Fatalf("expected explicit zero overrides in effective target, got %+v", target)
|
||||
intPointer := func(value int) *int {
|
||||
return &value
|
||||
}
|
||||
|
||||
defaultsProfile := executionProfileFixture{
|
||||
id: "settings-defaults",
|
||||
endpoint: "http://profile-defaults.test/v1",
|
||||
model: "profile-defaults-model",
|
||||
serviceTier: "profile-defaults-tier",
|
||||
reasoningEffort: "profile-defaults-reasoning",
|
||||
apiKeyEnv: "SCRIPTORIUM_PRECEDENCE_DEFAULTS",
|
||||
extraParamSource: "profile-defaults",
|
||||
}
|
||||
profileSettings := executionProfileFixture{
|
||||
id: "settings-profile",
|
||||
endpoint: "http://profile-settings.test/v1",
|
||||
model: "profile-settings-model",
|
||||
temperature: 0.31,
|
||||
maxTokens: 311,
|
||||
topP: 0.61,
|
||||
timeoutSeconds: 71,
|
||||
serviceTier: "profile-settings-tier",
|
||||
reasoningEffort: "profile-settings-reasoning",
|
||||
apiKeyEnv: "SCRIPTORIUM_PRECEDENCE_PROFILE",
|
||||
extraParamSource: "profile-settings",
|
||||
}
|
||||
requestProfile := executionProfileFixture{
|
||||
id: "settings-request",
|
||||
endpoint: "http://profile-request.test/v1",
|
||||
model: "profile-request-model",
|
||||
temperature: 0.29,
|
||||
maxTokens: 299,
|
||||
topP: 0.59,
|
||||
timeoutSeconds: 79,
|
||||
serviceTier: "profile-request-tier",
|
||||
reasoningEffort: "profile-request-reasoning",
|
||||
apiKeyEnv: "SCRIPTORIUM_PRECEDENCE_REQUEST_PROFILE",
|
||||
extraParamSource: "profile-request",
|
||||
}
|
||||
zeroOverrideProfile := executionProfileFixture{
|
||||
id: "settings-zero",
|
||||
endpoint: "http://profile-zero.test/v1",
|
||||
model: "profile-zero-model",
|
||||
temperature: 0.43,
|
||||
maxTokens: 433,
|
||||
topP: 0.73,
|
||||
timeoutSeconds: 83,
|
||||
serviceTier: "profile-zero-tier",
|
||||
reasoningEffort: "profile-zero-reasoning",
|
||||
apiKeyEnv: "SCRIPTORIUM_PRECEDENCE_ZERO",
|
||||
extraParamSource: "profile-zero",
|
||||
}
|
||||
|
||||
requestTarget := scriptorium.ExecutionTarget{
|
||||
Endpoint: "http://request-settings.test/v1",
|
||||
Model: "request-settings-model",
|
||||
Temperature: 0.87,
|
||||
MaxTokens: 877,
|
||||
TopP: 0.97,
|
||||
TimeoutSeconds: 177,
|
||||
ServiceTier: "request-settings-tier",
|
||||
ReasoningEffort: "request-settings-reasoning",
|
||||
APIKeyEnv: "SCRIPTORIUM_PRECEDENCE_REQUEST",
|
||||
ExtraParams: map[string]any{"source": "request-settings"},
|
||||
}
|
||||
zeroOverrideTarget := executionTargetFromProfileFixture(zeroOverrideProfile)
|
||||
zeroOverrideTarget.Temperature = 0
|
||||
zeroOverrideTarget.MaxTokens = 0
|
||||
zeroOverrideTarget.TopP = 0
|
||||
zeroOverrideTarget.TimeoutSeconds = 0
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
profile executionProfileFixture
|
||||
override *scriptorium.ExecutionTargetOverride
|
||||
want scriptorium.ExecutionTarget
|
||||
wantPresence scriptorium.ExecutionTargetPresence
|
||||
}{
|
||||
{
|
||||
name: "framework defaults fill zero-valued profile settings",
|
||||
profile: defaultsProfile,
|
||||
want: scriptorium.ExecutionTarget{
|
||||
Endpoint: defaultsProfile.endpoint,
|
||||
Model: defaultsProfile.model,
|
||||
Temperature: 0,
|
||||
MaxTokens: 0,
|
||||
TopP: 1,
|
||||
TimeoutSeconds: 600,
|
||||
ServiceTier: defaultsProfile.serviceTier,
|
||||
ReasoningEffort: defaultsProfile.reasoningEffort,
|
||||
APIKeyEnv: defaultsProfile.apiKeyEnv,
|
||||
ExtraParams: map[string]any{"source": defaultsProfile.extraParamSource},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "profile settings replace framework defaults",
|
||||
profile: profileSettings,
|
||||
want: executionTargetFromProfileFixture(profileSettings),
|
||||
},
|
||||
{
|
||||
name: "request settings replace profile settings",
|
||||
profile: requestProfile,
|
||||
override: &scriptorium.ExecutionTargetOverride{
|
||||
Endpoint: requestTarget.Endpoint,
|
||||
Model: requestTarget.Model,
|
||||
Temperature: floatPointer(requestTarget.Temperature),
|
||||
MaxTokens: intPointer(requestTarget.MaxTokens),
|
||||
TopP: floatPointer(requestTarget.TopP),
|
||||
TimeoutSeconds: intPointer(requestTarget.TimeoutSeconds),
|
||||
ServiceTier: requestTarget.ServiceTier,
|
||||
ReasoningEffort: requestTarget.ReasoningEffort,
|
||||
APIKeyEnv: requestTarget.APIKeyEnv,
|
||||
ExtraParams: requestTarget.ExtraParams,
|
||||
},
|
||||
want: requestTarget,
|
||||
wantPresence: scriptorium.ExecutionTargetPresence{Temperature: true, MaxTokens: true, TopP: true, TimeoutSeconds: true},
|
||||
},
|
||||
{
|
||||
name: "explicit request zero replaces profile settings",
|
||||
profile: zeroOverrideProfile,
|
||||
override: &scriptorium.ExecutionTargetOverride{
|
||||
Temperature: floatPointer(0),
|
||||
MaxTokens: intPointer(0),
|
||||
TopP: floatPointer(0),
|
||||
TimeoutSeconds: intPointer(0),
|
||||
},
|
||||
want: zeroOverrideTarget,
|
||||
wantPresence: scriptorium.ExecutionTargetPresence{Temperature: true, MaxTokens: true, TopP: true, TimeoutSeconds: true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(tt.profile.apiKeyEnv, "set")
|
||||
if tt.override != nil && tt.override.APIKeyEnv != "" {
|
||||
t.Setenv(tt.override.APIKeyEnv, "set")
|
||||
}
|
||||
|
||||
profileDir := t.TempDir()
|
||||
writeExecutionProfileFixture(t, profileDir, tt.profile)
|
||||
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}
|
||||
engine, err := scriptorium.NewEngine(scriptorium.Config{
|
||||
PromptDir: frameworkPromptDir,
|
||||
ProfileDir: profileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
}, scriptorium.WithLLMClient(fake))
|
||||
if err != nil {
|
||||
t.Fatalf("construct engine: %v", err)
|
||||
}
|
||||
|
||||
_, err = engine.Run(context.Background(), scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
ProfileID: tt.profile.id,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.Inline("Nia labels the archive."),
|
||||
"glossary": scriptorium.Inline("archive: A catalogued collection."),
|
||||
},
|
||||
Execution: tt.override,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run engine: %v", err)
|
||||
}
|
||||
if len(fake.requests) != 1 {
|
||||
t.Fatalf("expected one generation request, got %d", len(fake.requests))
|
||||
}
|
||||
got := fake.requests[0]
|
||||
if !reflect.DeepEqual(got.Target, tt.want) {
|
||||
t.Fatalf("unexpected effective target:\ngot=%#v\nwant=%#v", got.Target, tt.want)
|
||||
}
|
||||
if got.TargetPresence != tt.wantPresence {
|
||||
t.Fatalf("unexpected target presence: got=%+v want=%+v", got.TargetPresence, tt.wantPresence)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,25 +572,17 @@ func TestEngineRunWithDirectorySourcesAndFileInputs(t *testing.T) {
|
||||
func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||
const directKey = "direct-injected-key"
|
||||
fake := &fakeLLMClient{
|
||||
response: &scriptorium.GenerateResponse{Content: "ok"},
|
||||
response: &scriptorium.GenerateResponse{Content: `{"events":[{"title":"Archive labelled"}]}`},
|
||||
}
|
||||
engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
|
||||
zeroFloat := 0.0
|
||||
zeroInt := 0
|
||||
|
||||
_, err := engine.Run(context.Background(), scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
PromptID: frameworkStructuredEventsPromptID,
|
||||
APIKey: directKey,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||
},
|
||||
Execution: &scriptorium.ExecutionTargetOverride{
|
||||
Temperature: &zeroFloat,
|
||||
MaxTokens: &zeroInt,
|
||||
TopP: &zeroFloat,
|
||||
TimeoutSeconds: &zeroInt,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected run to succeed, got %v", err)
|
||||
@@ -449,14 +594,8 @@ func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||
if len(req.Prompt.Messages) != 2 || !strings.Contains(req.Prompt.Messages[1].Content, "Rin opens the gate.") {
|
||||
t.Fatalf("expected rendered prompt in generate request, got %+v", req.Prompt)
|
||||
}
|
||||
if req.Target.Model != "contract-fast-model" || req.Target.Temperature != 0 || req.Target.MaxTokens != 0 || req.Target.TopP != 0 || req.Target.TimeoutSeconds != 0 {
|
||||
t.Fatalf("unexpected effective target: %+v", req.Target)
|
||||
}
|
||||
if !req.TargetPresence.Temperature || !req.TargetPresence.MaxTokens || !req.TargetPresence.TopP || !req.TargetPresence.TimeoutSeconds {
|
||||
t.Fatalf("expected explicit zero target presence, got %+v", req.TargetPresence)
|
||||
}
|
||||
if req.StructuredOutput != nil {
|
||||
t.Fatalf("did not expect structured output for markdown prompt: %+v", req.StructuredOutput)
|
||||
if req.StructuredOutput == nil || req.StructuredOutput.Type != scriptorium.StructuredOutputJSONSchema || req.StructuredOutput.JSONSchema == nil {
|
||||
t.Fatalf("expected structured output handoff, got %+v", req.StructuredOutput)
|
||||
}
|
||||
if req.APIKey != directKey {
|
||||
t.Fatalf("expected direct key on injected generate request")
|
||||
@@ -470,6 +609,78 @@ func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineRunPropagatesCallerCancellation(t *testing.T) {
|
||||
started := make(chan struct{})
|
||||
transport := roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
close(started)
|
||||
<-req.Context().Done()
|
||||
return nil, req.Context().Err()
|
||||
})
|
||||
engine, err := scriptorium.NewEngine(scriptorium.Config{
|
||||
PromptDir: frameworkPromptDir,
|
||||
ProfileDir: frameworkProfileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct engine: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := engine.Run(ctx, scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.Inline("Nia labels the archive."),
|
||||
"glossary": scriptorium.Inline("archive: A catalogued collection."),
|
||||
},
|
||||
})
|
||||
result <- err
|
||||
}()
|
||||
|
||||
<-started
|
||||
cancel()
|
||||
if err := <-result; !errors.Is(err, scriptorium.ErrLLMGenerate) {
|
||||
t.Fatalf("expected ErrLLMGenerate after caller cancellation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsReservedExtraParamsBeforeProviderCall(t *testing.T) {
|
||||
called := false
|
||||
transport := roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
called = true
|
||||
return nil, errors.New("provider should not be called")
|
||||
})
|
||||
engine, err := scriptorium.NewEngine(scriptorium.Config{
|
||||
PromptDir: frameworkPromptDir,
|
||||
ProfileDir: frameworkProfileDir,
|
||||
SchemaDir: frameworkSchemaDir,
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct engine: %v", err)
|
||||
}
|
||||
|
||||
_, err = engine.Run(context.Background(), scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.Inline("Nia labels the archive."),
|
||||
"glossary": scriptorium.Inline("archive: A catalogued collection."),
|
||||
},
|
||||
Execution: &scriptorium.ExecutionTargetOverride{
|
||||
ExtraParams: map[string]any{"model": "collision"},
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, scriptorium.ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("expected reserved provider parameter to fail before the provider call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUsesDirectAPIKeyWithDefaultLLMClient(t *testing.T) {
|
||||
const directKey = "direct-public-key"
|
||||
const missingEnv = "SCRIPTORIUM_PUBLIC_DIRECT_MISSING"
|
||||
@@ -702,6 +913,18 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
|
||||
client: &fakeLLMClient{err: llmErr},
|
||||
want: scriptorium.ErrLLMGenerate,
|
||||
},
|
||||
{
|
||||
name: "nil llm response",
|
||||
req: scriptorium.RunRequest{
|
||||
PromptID: frameworkMarkdownSummaryPromptID,
|
||||
Inputs: map[string]scriptorium.ArtifactRef{
|
||||
"transcript": scriptorium.Inline("Rin opens the gate."),
|
||||
"glossary": scriptorium.Inline("gate: A guarded passage."),
|
||||
},
|
||||
},
|
||||
client: &fakeLLMClient{},
|
||||
want: scriptorium.ErrLLMGenerate,
|
||||
},
|
||||
{
|
||||
name: "validation runtime failure",
|
||||
req: scriptorium.RunRequest{
|
||||
@@ -2057,6 +2280,67 @@ func publicSchemaJSON() string {
|
||||
}`
|
||||
}
|
||||
|
||||
type executionProfileFixture struct {
|
||||
id string
|
||||
endpoint string
|
||||
model string
|
||||
temperature float64
|
||||
maxTokens int
|
||||
topP float64
|
||||
timeoutSeconds int
|
||||
serviceTier string
|
||||
reasoningEffort string
|
||||
apiKeyEnv string
|
||||
extraParamSource string
|
||||
}
|
||||
|
||||
func executionTargetFromProfileFixture(profile executionProfileFixture) scriptorium.ExecutionTarget {
|
||||
return scriptorium.ExecutionTarget{
|
||||
Endpoint: profile.endpoint,
|
||||
Model: profile.model,
|
||||
Temperature: profile.temperature,
|
||||
MaxTokens: profile.maxTokens,
|
||||
TopP: profile.topP,
|
||||
TimeoutSeconds: profile.timeoutSeconds,
|
||||
ServiceTier: profile.serviceTier,
|
||||
ReasoningEffort: profile.reasoningEffort,
|
||||
APIKeyEnv: profile.apiKeyEnv,
|
||||
ExtraParams: map[string]any{"source": profile.extraParamSource},
|
||||
}
|
||||
}
|
||||
|
||||
func writeExecutionProfileFixture(t *testing.T, dir string, profile executionProfileFixture) {
|
||||
t.Helper()
|
||||
data := fmt.Sprintf(`id: %s
|
||||
endpoint: %s
|
||||
model: %s
|
||||
temperature: %g
|
||||
max_tokens: %d
|
||||
top_p: %g
|
||||
timeout_seconds: %d
|
||||
service_tier: %s
|
||||
reasoning_effort: %s
|
||||
api_key_env: %s
|
||||
extra_params:
|
||||
source: %q
|
||||
`,
|
||||
profile.id,
|
||||
profile.endpoint,
|
||||
profile.model,
|
||||
profile.temperature,
|
||||
profile.maxTokens,
|
||||
profile.topP,
|
||||
profile.timeoutSeconds,
|
||||
profile.serviceTier,
|
||||
profile.reasoningEffort,
|
||||
profile.apiKeyEnv,
|
||||
profile.extraParamSource,
|
||||
)
|
||||
if err := os.WriteFile(filepath.Join(dir, profile.id+".yaml"), []byte(data), 0o644); err != nil {
|
||||
t.Fatalf("write execution profile fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLLMClient struct {
|
||||
response *scriptorium.GenerateResponse
|
||||
err error
|
||||
|
||||
Reference in New Issue
Block a user