Move framework tests to contract fixtures

This commit is contained in:
2026-07-27 22:04:08 +00:00
parent 2bbf13e739
commit c6c747e94d
2 changed files with 192 additions and 257 deletions

View File

@@ -36,14 +36,14 @@ const (
) )
func TestNewEngineRejectsMissingPromptDir(t *testing.T) { func TestNewEngineRejectsMissingPromptDir(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{ProfileDir: "./examples/profiles"}) _, err := scriptorium.NewEngine(scriptorium.Config{ProfileDir: frameworkProfileDir})
if !errors.Is(err, scriptorium.ErrInvalidConfig) { if !errors.Is(err, scriptorium.ErrInvalidConfig) {
t.Fatalf("expected ErrInvalidConfig, got %v", err) t.Fatalf("expected ErrInvalidConfig, got %v", err)
} }
} }
func TestNewEngineAcceptsMissingProfileDir(t *testing.T) { func TestNewEngineAcceptsMissingProfileDir(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}) _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir})
if err != nil { if err != nil {
t.Fatalf("expected missing profile dir to use built-ins, got %v", err) t.Fatalf("expected missing profile dir to use built-ins, got %v", err)
} }
@@ -135,10 +135,10 @@ func TestPrepareWorksWithFrameworkContractCorpus(t *testing.T) {
} }
func TestPrepareWorksWithInlineInputs(t *testing.T) { func TestPrepareWorksWithInlineInputs(t *testing.T) {
engine := newExampleEngine(t) engine := newContractEngine(t)
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin scouts the tower.\nKara lights a lantern."), "transcript": scriptorium.Inline("Rin scouts the tower.\nKara lights a lantern."),
"glossary": scriptorium.InlineWithURI("memory://glossary.yml", "party:\n - Rin\n - Kara\n"), "glossary": scriptorium.InlineWithURI("memory://glossary.yml", "party:\n - Rin\n - Kara\n"),
@@ -161,12 +161,22 @@ func TestPreparedRunJSONDoesNotExposeSecretOrTargetPresence(t *testing.T) {
const secret = "public-api-test-secret" const secret = "public-api-test-secret"
t.Setenv(envName, secret) t.Setenv(envName, secret)
engine := newExampleEngine(t) profileDir := t.TempDir()
writePublicProfileFileWithAPIKeyEnv(t, profileDir, "prepared-secret", "http://localhost:8000/v1", "prepared-secret-model", envName)
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: frameworkPromptDir,
ProfileDir: profileDir,
SchemaDir: frameworkSchemaDir,
})
if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err)
}
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.structured_events", PromptID: frameworkStructuredEventsPromptID,
ProfileID: "prepared-secret",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File("./examples/fixtures/transcript.md"), "transcript": scriptorium.File(frameworkTranscriptPath),
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"), "glossary": scriptorium.File(frameworkGlossaryPath),
}, },
}) })
if err != nil { if err != nil {
@@ -194,8 +204,8 @@ func TestPreparedRunJSONDoesNotExposeSecretOrTargetPresence(t *testing.T) {
func TestRunRequestFormattingRedactsDirectAPIKey(t *testing.T) { func TestRunRequestFormattingRedactsDirectAPIKey(t *testing.T) {
const secret = "run-request-secret" const secret = "run-request-secret"
req := scriptorium.RunRequest{ req := scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "local-fast", ProfileID: frameworkFastProfileID,
APIKey: secret, APIKey: secret,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -265,15 +275,15 @@ func TestGenerateRequestFormattingRedactsDirectAPIKey(t *testing.T) {
} }
func TestPreparePreservesExplicitZeroExecutionOverrides(t *testing.T) { func TestPreparePreservesExplicitZeroExecutionOverrides(t *testing.T) {
engine := newExampleEngine(t) engine := newContractEngine(t)
zeroFloat := 0.0 zeroFloat := 0.0
zeroInt := 0 zeroInt := 0
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File("./examples/fixtures/transcript.md"), "transcript": scriptorium.File(frameworkTranscriptPath),
"glossary": scriptorium.File("./examples/fixtures/glossary.yml"), "glossary": scriptorium.File(frameworkGlossaryPath),
}, },
Execution: &scriptorium.ExecutionTargetOverride{ Execution: &scriptorium.ExecutionTargetOverride{
Temperature: &zeroFloat, Temperature: &zeroFloat,
@@ -308,10 +318,10 @@ func TestRunSucceedsWithInjectedLLMClient(t *testing.T) {
}, },
}, },
} }
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
result, err := engine.Run(context.Background(), scriptorium.RunRequest{ result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -338,7 +348,7 @@ func TestRunSucceedsWithInjectedLLMClient(t *testing.T) {
if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid { if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid {
t.Fatalf("expected passed validation, got %+v", result.Validation) t.Fatalf("expected passed validation, got %+v", result.Validation)
} }
if result.PromptID != "generic.markdown_summary" || result.SelectedProfileID != "local-fast" || result.ModelName != "gpt-4o-mini" { if result.PromptID != frameworkMarkdownSummaryPromptID || result.SelectedProfileID != frameworkFastProfileID || result.ModelName != "contract-fast-model" {
t.Fatalf("unexpected run metadata: %+v", result) t.Fatalf("unexpected run metadata: %+v", result)
} }
if result.Usage.TotalTokens != 15 || result.Usage.CachedTokens != 3 || result.Usage.CacheWriteTokens != 2 { if result.Usage.TotalTokens != 15 || result.Usage.CachedTokens != 3 || result.Usage.CacheWriteTokens != 2 {
@@ -354,17 +364,69 @@ func TestRunSucceedsWithInjectedLLMClient(t *testing.T) {
} }
} }
func TestEngineRunWithDirectorySourcesAndFileInputs(t *testing.T) {
fake := &fakeLLMClient{
response: &scriptorium.GenerateResponse{
Content: `{"events":[{"title":"Archive labelled"}]}`,
Usage: scriptorium.TokenUsage{
PromptTokens: 42,
CompletionTokens: 36,
TotalTokens: 78,
},
},
}
engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: frameworkStructuredEventsPromptID,
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File(frameworkTranscriptPath),
"glossary": scriptorium.File(frameworkGlossaryPath),
},
})
if err != nil {
t.Fatalf("expected run to succeed, got %v", err)
}
if result.PromptID != frameworkStructuredEventsPromptID || result.SelectedProfileID != frameworkQualityProfileID {
t.Fatalf("unexpected run metadata: %+v", result)
}
if result.RunID == "" || result.PromptHash == "" || result.RenderedPromptHash == "" {
t.Fatalf("expected run and prompt hashes, got %+v", result)
}
if result.InputHashes["transcript"] == "" || result.InputHashes["glossary"] == "" {
t.Fatalf("expected both input hashes, got %#v", result.InputHashes)
}
if len(fake.requests) != 1 || fake.requests[0].StructuredOutput == nil ||
fake.requests[0].StructuredOutput.Type != scriptorium.StructuredOutputJSONSchema ||
fake.requests[0].StructuredOutput.JSONSchema == nil ||
fake.requests[0].StructuredOutput.JSONSchema.Schema == nil {
t.Fatalf("expected provider JSON Schema structured output, got %+v", fake.requests)
}
if result.Validation.Status != scriptorium.ValidationPassed || !result.Validation.IsValid || result.Validation.Mode != scriptorium.ValidationJSONSchema {
t.Fatalf("expected passed JSON Schema validation, got %+v", result.Validation)
}
if result.Artifact.ContentType != "application/json" {
t.Fatalf("expected JSON artifact, got %q", result.Artifact.ContentType)
}
if result.RawOutput != fake.response.Content || result.Usage != fake.response.Usage {
t.Fatalf("expected preserved output and usage, got output=%q usage=%+v", result.RawOutput, result.Usage)
}
if result.StartTime.IsZero() || result.EndTime.IsZero() || result.EndTime.Before(result.StartTime) || result.Duration < 0 {
t.Fatalf("expected ordered non-zero timestamps and non-negative duration, got start=%v end=%v duration=%v", result.StartTime, result.EndTime, result.Duration)
}
}
func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) { func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
const directKey = "direct-injected-key" const directKey = "direct-injected-key"
fake := &fakeLLMClient{ fake := &fakeLLMClient{
response: &scriptorium.GenerateResponse{Content: "ok"}, response: &scriptorium.GenerateResponse{Content: "ok"},
} }
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
zeroFloat := 0.0 zeroFloat := 0.0
zeroInt := 0 zeroInt := 0
_, err := engine.Run(context.Background(), scriptorium.RunRequest{ _, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
APIKey: directKey, APIKey: directKey,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -387,7 +449,7 @@ func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
if len(req.Prompt.Messages) != 2 || !strings.Contains(req.Prompt.Messages[1].Content, "Rin opens the gate.") { 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) t.Fatalf("expected rendered prompt in generate request, got %+v", req.Prompt)
} }
if req.Target.Model != "gpt-4o-mini" || req.Target.Temperature != 0 || req.Target.MaxTokens != 0 || req.Target.TopP != 0 || req.Target.TimeoutSeconds != 0 { 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) t.Fatalf("unexpected effective target: %+v", req.Target)
} }
if !req.TargetPresence.Temperature || !req.TargetPresence.MaxTokens || !req.TargetPresence.TopP || !req.TargetPresence.TimeoutSeconds { if !req.TargetPresence.Temperature || !req.TargetPresence.MaxTokens || !req.TargetPresence.TopP || !req.TargetPresence.TimeoutSeconds {
@@ -430,16 +492,16 @@ func TestRunUsesDirectAPIKeyWithDefaultLLMClient(t *testing.T) {
profileDir := t.TempDir() profileDir := t.TempDir()
writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-auth", server.URL+"/v1", "test-model", missingEnv) writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-auth", server.URL+"/v1", "test-model", missingEnv)
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
result, err := engine.Run(context.Background(), scriptorium.RunRequest{ result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "direct-auth", ProfileID: "direct-auth",
APIKey: directKey, APIKey: directKey,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
@@ -475,16 +537,16 @@ func TestPrepareDirectAPIKeyBypassesMissingEnvWithoutLeakingOrHashing(t *testing
profileDir := t.TempDir() profileDir := t.TempDir()
writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-prepare", "http://localhost:8000/v1", "test-model", missingEnv) writePublicProfileFileWithAPIKeyEnv(t, profileDir, "direct-prepare", "http://localhost:8000/v1", "test-model", missingEnv)
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
baseReq := scriptorium.RunRequest{ baseReq := scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "direct-prepare", ProfileID: "direct-prepare",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -527,16 +589,16 @@ func TestMissingCredentialsFailClearlyWhenProfileRequiresAuth(t *testing.T) {
profileDir := t.TempDir() profileDir := t.TempDir()
writePublicProfileFileWithAPIKeyEnv(t, profileDir, "requires-auth", "http://localhost:8000/v1", "test-model", missingEnv) writePublicProfileFileWithAPIKeyEnv(t, profileDir, "requires-auth", "http://localhost:8000/v1", "test-model", missingEnv)
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "requires-auth", ProfileID: "requires-auth",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -555,10 +617,10 @@ func TestRunValidationFailureReturnsResult(t *testing.T) {
fake := &fakeLLMClient{ fake := &fakeLLMClient{
response: &scriptorium.GenerateResponse{Content: ""}, response: &scriptorium.GenerateResponse{Content: ""},
} }
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
result, err := engine.Run(context.Background(), scriptorium.RunRequest{ result, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -600,7 +662,7 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
{ {
name: "profile not found", name: "profile not found",
req: scriptorium.RunRequest{ req: scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "missing-profile", ProfileID: "missing-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -612,9 +674,9 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
{ {
name: "artifact load", name: "artifact load",
req: scriptorium.RunRequest{ req: scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File("./examples/fixtures/does-not-exist.md"), "transcript": scriptorium.File(filepath.Join(t.TempDir(), "does-not-exist.md")),
}, },
}, },
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}, client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
@@ -623,7 +685,7 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
{ {
name: "prompt render", name: "prompt render",
req: scriptorium.RunRequest{ req: scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
}, },
client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}, client: &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}},
want: scriptorium.ErrPromptRender, want: scriptorium.ErrPromptRender,
@@ -631,7 +693,7 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
{ {
name: "llm failure", name: "llm failure",
req: scriptorium.RunRequest{ req: scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -643,7 +705,7 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
{ {
name: "validation runtime failure", name: "validation runtime failure",
req: scriptorium.RunRequest{ req: scriptorium.RunRequest{
PromptID: "generic.structured_events", PromptID: frameworkStructuredEventsPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
}, },
@@ -659,9 +721,9 @@ func TestPublicErrorsSupportErrorsIs(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
schemaDir := tc.schemaDir schemaDir := tc.schemaDir
if schemaDir == "" { if schemaDir == "" {
schemaDir = "./examples/schemas" schemaDir = frameworkSchemaDir
} }
engine := newExampleEngineWithOptions(t, schemaDir, scriptorium.WithLLMClient(tc.client)) engine := newContractEngineWithOptions(t, schemaDir, scriptorium.WithLLMClient(tc.client))
_, err := engine.Run(context.Background(), tc.req) _, err := engine.Run(context.Background(), tc.req)
if !errors.Is(err, tc.want) { if !errors.Is(err, tc.want) {
t.Fatalf("expected errors.Is(%v), got %v", tc.want, err) t.Fatalf("expected errors.Is(%v), got %v", tc.want, err)
@@ -682,16 +744,16 @@ api_key: secret
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "raw-profile", ProfileID: "raw-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -716,16 +778,16 @@ unknown_field: true
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "broken-profile", ProfileID: "broken-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -744,15 +806,15 @@ func TestPromptRepositoryReadFailureMapsToPromptLoad(t *testing.T) {
missingPromptDir := filepath.Join(t.TempDir(), "missing-prompts") missingPromptDir := filepath.Join(t.TempDir(), "missing-prompts")
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: missingPromptDir, PromptDir: missingPromptDir,
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -769,17 +831,17 @@ func TestPromptRepositoryReadFailureMapsToPromptLoad(t *testing.T) {
func TestSelectedProfileRepositoryReadFailureMapsToProfileLoad(t *testing.T) { func TestSelectedProfileRepositoryReadFailureMapsToProfileLoad(t *testing.T) {
missingProfileDir := filepath.Join(t.TempDir(), "missing-profiles") missingProfileDir := filepath.Join(t.TempDir(), "missing-profiles")
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: missingProfileDir, ProfileDir: missingProfileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "local-fast", ProfileID: frameworkFastProfileID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -796,15 +858,15 @@ func TestSelectedProfileRepositoryReadFailureMapsToProfileLoad(t *testing.T) {
func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) { func TestPrepareUsesBuiltInProfileWithoutProfileDir(t *testing.T) {
t.Setenv("OPENROUTER_API_KEY", "test-key") t.Setenv("OPENROUTER_API_KEY", "test-key")
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -852,16 +914,16 @@ func TestCustomProfileOverridesBuiltInProfile(t *testing.T) {
writePublicProfileFile(t, profileDir, "mistral-small-3", "http://localhost:8000/v1", "custom-model") writePublicProfileFile(t, profileDir, "mistral-small-3", "http://localhost:8000/v1", "custom-model")
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -889,16 +951,16 @@ unexpected: true
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: profileDir, ProfileDir: profileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}) })
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -915,7 +977,7 @@ func TestPrepareWorksWithPromptFSAndRelativeContentFile(t *testing.T) {
"assets/prompts/fs-summary.yaml": &fstest.MapFile{Data: []byte(` "assets/prompts/fs-summary.yaml": &fstest.MapFile{Data: []byte(`
id: fs.summary id: fs.summary
version: "1.0.0" version: "1.0.0"
default_profile: local-fast default_profile: contract-fast
inputs: inputs:
- name: transcript - name: transcript
required: true required: true
@@ -932,8 +994,8 @@ output:
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: t.TempDir(), PromptDir: t.TempDir(),
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithPromptFS(promptFS, "assets/prompts")) }, scriptorium.WithPromptFS(promptFS, "assets/prompts"))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
@@ -958,7 +1020,7 @@ func TestPrepareWithPromptFSRejectsEscapedContentFile(t *testing.T) {
"assets/prompts/fs-escape.yaml": &fstest.MapFile{Data: []byte(` "assets/prompts/fs-escape.yaml": &fstest.MapFile{Data: []byte(`
id: fs.escape id: fs.escape
version: "1.0.0" version: "1.0.0"
default_profile: local-fast default_profile: contract-fast
messages: messages:
- role: user - role: user
content_file: ../outside.tmpl content_file: ../outside.tmpl
@@ -972,8 +1034,8 @@ output:
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: t.TempDir(), PromptDir: t.TempDir(),
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithPromptFS(promptFS, "assets/prompts")) }, scriptorium.WithPromptFS(promptFS, "assets/prompts"))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
@@ -991,7 +1053,7 @@ func TestPrepareWorksWithPromptFile(t *testing.T) {
if err := os.WriteFile(promptPath, []byte(` if err := os.WriteFile(promptPath, []byte(`
id: single.file.prompt id: single.file.prompt
version: "1.0.0" version: "1.0.0"
default_profile: local-fast default_profile: contract-fast
inputs: inputs:
- name: transcript - name: transcript
required: true required: true
@@ -1010,8 +1072,8 @@ output:
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithPromptFile(promptPath)) }, scriptorium.WithPromptFile(promptPath))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
@@ -1044,15 +1106,15 @@ model: profile-fs-model
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfileFS(profileFS, "profiles")) }, scriptorium.WithProfileFS(profileFS, "profiles"))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1079,15 +1141,15 @@ model: profile-file-model
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfileFile(profilePath)) }, scriptorium.WithProfileFile(profilePath))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1104,8 +1166,8 @@ model: profile-file-model
func TestPrepareWorksWithInMemoryProfilesWithoutProfileFiles(t *testing.T) { func TestPrepareWorksWithInMemoryProfilesWithoutProfileFiles(t *testing.T) {
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfiles(scriptorium.Profile{ }, scriptorium.WithProfiles(scriptorium.Profile{
ID: "memory-profile", ID: "memory-profile",
Endpoint: "http://memory-profile/v1", Endpoint: "http://memory-profile/v1",
@@ -1116,7 +1178,7 @@ func TestPrepareWorksWithInMemoryProfilesWithoutProfileFiles(t *testing.T) {
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "memory-profile", ProfileID: "memory-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1141,8 +1203,8 @@ model: profile-fs-model
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, },
scriptorium.WithProfileFS(profileFS, "profiles"), scriptorium.WithProfileFS(profileFS, "profiles"),
scriptorium.WithProfiles(scriptorium.Profile{ scriptorium.WithProfiles(scriptorium.Profile{
@@ -1156,7 +1218,7 @@ model: profile-fs-model
} }
prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{ prepared, err := engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "mistral-small-3", ProfileID: "mistral-small-3",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1172,7 +1234,7 @@ model: profile-fs-model
} }
func TestWithProfilesRejectsDuplicateIDs(t *testing.T) { func TestWithProfilesRejectsDuplicateIDs(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir},
scriptorium.WithProfiles( scriptorium.WithProfiles(
scriptorium.Profile{ID: "duplicate", Endpoint: "http://one/v1", Model: "one"}, scriptorium.Profile{ID: "duplicate", Endpoint: "http://one/v1", Model: "one"},
scriptorium.Profile{ID: "duplicate", Endpoint: "http://two/v1", Model: "two"}, scriptorium.Profile{ID: "duplicate", Endpoint: "http://two/v1", Model: "two"},
@@ -1196,15 +1258,15 @@ func TestOpenAICompatibleProfileRunsThroughNormalProfilePath(t *testing.T) {
}) })
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfiles(prof), scriptorium.WithLLMClient(fake)) }, scriptorium.WithProfiles(prof), scriptorium.WithLLMClient(fake))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
_, err = engine.Run(context.Background(), scriptorium.RunRequest{ _, err = engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "template-profile", ProfileID: "template-profile",
APIKey: "template-key", APIKey: "template-key",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
@@ -1313,8 +1375,8 @@ func TestEngineRunLayersTransportAndGenerationTimeouts(t *testing.T) {
Transport: transport, Transport: transport,
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
Timeout: tc.configTimeout, Timeout: tc.configTimeout,
HTTPClient: httpClient, HTTPClient: httpClient,
}, scriptorium.WithProfiles(scriptorium.Profile{ }, scriptorium.WithProfiles(scriptorium.Profile{
@@ -1335,7 +1397,7 @@ func TestEngineRunLayersTransportAndGenerationTimeouts(t *testing.T) {
defer cancel() defer cancel()
_, err = engine.Run(ctx, scriptorium.RunRequest{ _, err = engine.Run(ctx, scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "layered-timeout", ProfileID: "layered-timeout",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1376,7 +1438,7 @@ func TestOpenAICompatibleProfileDefersExtraParamsValidation(t *testing.T) {
ExtraParams: cyclic, ExtraParams: cyclic,
}) })
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir},
scriptorium.WithProfiles(prof), scriptorium.WithProfiles(prof),
) )
if !errors.Is(err, scriptorium.ErrInvalidConfig) { if !errors.Is(err, scriptorium.ErrInvalidConfig) {
@@ -1402,8 +1464,8 @@ func TestOpenAICompatibleProfileNestedExtraParamsRunThroughWithProfiles(t *testi
extraParams["added"] = "mutated-after-construction" extraParams["added"] = "mutated-after-construction"
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfiles(prof), scriptorium.WithLLMClient(fake)) }, scriptorium.WithProfiles(prof), scriptorium.WithLLMClient(fake))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
@@ -1411,7 +1473,7 @@ func TestOpenAICompatibleProfileNestedExtraParamsRunThroughWithProfiles(t *testi
nested["added"] = "mutated-after-construction" nested["added"] = "mutated-after-construction"
_, err = engine.Run(context.Background(), scriptorium.RunRequest{ _, err = engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "nested-template-profile", ProfileID: "nested-template-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1434,8 +1496,8 @@ func TestOpenAICompatibleProfileNestedExtraParamsRunThroughWithProfiles(t *testi
func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) { func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) {
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfiles(scriptorium.Profile{ }, scriptorium.WithProfiles(scriptorium.Profile{
ID: "requires-key", ID: "requires-key",
Endpoint: "http://requires-key/v1", Endpoint: "http://requires-key/v1",
@@ -1447,7 +1509,7 @@ func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) {
} }
req := scriptorium.RunRequest{ req := scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "requires-key", ProfileID: "requires-key",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1466,8 +1528,8 @@ func TestInMemoryProfileAPIKeyRequiredBehavior(t *testing.T) {
func TestInMemoryProfileWithoutAPIKeyRequiredWorksWithoutKey(t *testing.T) { func TestInMemoryProfileWithoutAPIKeyRequiredWorksWithoutKey(t *testing.T) {
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, scriptorium.WithProfiles(scriptorium.Profile{ }, scriptorium.WithProfiles(scriptorium.Profile{
ID: "no-key-required", ID: "no-key-required",
Endpoint: "http://no-key/v1", Endpoint: "http://no-key/v1",
@@ -1478,7 +1540,7 @@ func TestInMemoryProfileWithoutAPIKeyRequiredWorksWithoutKey(t *testing.T) {
} }
_, err = engine.Prepare(context.Background(), scriptorium.RunRequest{ _, err = engine.Prepare(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "no-key-required", ProfileID: "no-key-required",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1500,8 +1562,8 @@ func TestInMemoryProfileExtraParamsAreCopiedAcrossPublicBoundary(t *testing.T) {
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, },
scriptorium.WithProfiles(scriptorium.Profile{ scriptorium.WithProfiles(scriptorium.Profile{
ID: "copy-profile", ID: "copy-profile",
@@ -1519,7 +1581,7 @@ func TestInMemoryProfileExtraParamsAreCopiedAcrossPublicBoundary(t *testing.T) {
extraParams["added"] = "mutated" extraParams["added"] = "mutated"
_, err = engine.Run(context.Background(), scriptorium.RunRequest{ _, err = engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "copy-profile", ProfileID: "copy-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1553,7 +1615,7 @@ func TestWithProfilesRejectsInvalidExtraParams(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir},
scriptorium.WithProfiles(scriptorium.Profile{ scriptorium.WithProfiles(scriptorium.Profile{
ID: "invalid-extra-params", ID: "invalid-extra-params",
Endpoint: "http://invalid/v1", Endpoint: "http://invalid/v1",
@@ -1583,7 +1645,7 @@ func TestWithProfilesRejectsCyclicExtraParams(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir},
scriptorium.WithProfiles(scriptorium.Profile{ scriptorium.WithProfiles(scriptorium.Profile{
ID: "cyclic-extra-params", ID: "cyclic-extra-params",
Endpoint: "http://cyclic/v1", Endpoint: "http://cyclic/v1",
@@ -1602,7 +1664,7 @@ func TestRunStructuredOutputWorksWithSchemaFS(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}} fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}}
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: t.TempDir(), PromptDir: t.TempDir(),
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: t.TempDir(), SchemaDir: t.TempDir(),
}, },
scriptorium.WithPromptFS(publicStructuredPromptFS("schema.fs.prompt", "events.schema.json"), "prompts"), scriptorium.WithPromptFS(publicStructuredPromptFS("schema.fs.prompt", "events.schema.json"), "prompts"),
@@ -1639,7 +1701,7 @@ func TestRunStructuredOutputWorksWithSchemaFile(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}} fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: `{"events":[]}`}}
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
}, },
scriptorium.WithPromptFS(publicStructuredPromptFS("schema.file.prompt", "events.schema.json"), "prompts"), scriptorium.WithPromptFS(publicStructuredPromptFS("schema.file.prompt", "events.schema.json"), "prompts"),
scriptorium.WithSchemaFile(schemaPath), scriptorium.WithSchemaFile(schemaPath),
@@ -1686,7 +1748,7 @@ func TestSourceOptionsRejectInvalidInputs(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: "./examples/prompts"}, tc.opt) _, err := scriptorium.NewEngine(scriptorium.Config{PromptDir: frameworkPromptDir}, tc.opt)
if !errors.Is(err, scriptorium.ErrInvalidConfig) { if !errors.Is(err, scriptorium.ErrInvalidConfig) {
t.Fatalf("expected ErrInvalidConfig, got %v", err) t.Fatalf("expected ErrInvalidConfig, got %v", err)
} }
@@ -1707,15 +1769,15 @@ func TestPackageOptionsComposeFromSlice(t *testing.T) {
} }
engine, err := scriptorium.NewEngine(scriptorium.Config{ engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
SchemaDir: "./examples/schemas", SchemaDir: frameworkSchemaDir,
}, options...) }, options...)
if err != nil { if err != nil {
t.Fatalf("expected package-provided options to compose, got %v", err) t.Fatalf("expected package-provided options to compose, got %v", err)
} }
_, err = engine.Run(context.Background(), scriptorium.RunRequest{ _, err = engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
ProfileID: "slice-profile", ProfileID: "slice-profile",
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
@@ -1735,7 +1797,7 @@ func TestPackageOptionsComposeFromSlice(t *testing.T) {
func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T) { func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
labels := map[string]string{"route": "primary"} labels := map[string]string{"route": "primary"}
counts := map[string]int{"retry_budget": 2} counts := map[string]int{"retry_budget": 2}
@@ -1754,7 +1816,7 @@ func TestExtraParamsTypedNestedValuesAreCopiedAcrossPublicBoundary(t *testing.T)
} }
_, err := engine.Run(context.Background(), scriptorium.RunRequest{ _, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -1807,10 +1869,10 @@ func TestRunRejectsInvalidExtraParams(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
_, err := engine.Run(context.Background(), scriptorium.RunRequest{ _, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -1843,10 +1905,10 @@ func TestRunRejectsCyclicExtraParams(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}} fake := &fakeLLMClient{response: &scriptorium.GenerateResponse{Content: "ok"}}
engine := newExampleEngineWithOptions(t, "./examples/schemas", scriptorium.WithLLMClient(fake)) engine := newContractEngineWithOptions(t, frameworkSchemaDir, scriptorium.WithLLMClient(fake))
_, err := engine.Run(context.Background(), scriptorium.RunRequest{ _, err := engine.Run(context.Background(), scriptorium.RunRequest{
PromptID: "generic.markdown_summary", PromptID: frameworkMarkdownSummaryPromptID,
Inputs: map[string]scriptorium.ArtifactRef{ Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.Inline("Rin opens the gate."), "transcript": scriptorium.Inline("Rin opens the gate."),
"glossary": scriptorium.Inline("gate: A guarded passage."), "glossary": scriptorium.Inline("gate: A guarded passage."),
@@ -1864,52 +1926,52 @@ func TestRunRejectsCyclicExtraParams(t *testing.T) {
} }
func TestWithLLMClientRejectsNilClient(t *testing.T) { func TestWithLLMClientRejectsNilClient(t *testing.T) {
_, err := scriptorium.NewEngine(exampleConfig("./examples/schemas"), scriptorium.WithLLMClient(nil)) _, err := scriptorium.NewEngine(contractConfig(frameworkSchemaDir), scriptorium.WithLLMClient(nil))
if !errors.Is(err, scriptorium.ErrInvalidConfig) { if !errors.Is(err, scriptorium.ErrInvalidConfig) {
t.Fatalf("expected ErrInvalidConfig, got %v", err) t.Fatalf("expected ErrInvalidConfig, got %v", err)
} }
} }
func TestNewEngineConstructsDefaultLLMClientWithoutCredentials(t *testing.T) { func TestNewEngineConstructsDefaultLLMClientWithoutCredentials(t *testing.T) {
if _, err := scriptorium.NewEngine(exampleConfig("./examples/schemas")); err != nil { if _, err := scriptorium.NewEngine(contractConfig(frameworkSchemaDir)); err != nil {
t.Fatalf("expected default engine construction without credentials to succeed, got %v", err) t.Fatalf("expected default engine construction without credentials to succeed, got %v", err)
} }
} }
func newExampleEngine(t *testing.T) *scriptorium.Engine { func newContractEngine(t *testing.T) *scriptorium.Engine {
t.Helper() t.Helper()
for _, path := range []string{ for _, path := range []string{
"./examples/prompts", frameworkPromptDir,
"./examples/profiles", frameworkProfileDir,
"./examples/schemas", frameworkSchemaDir,
} { } {
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
t.Fatalf("expected example path %s to exist: %v", path, err) t.Fatalf("expected framework contract path %s to exist: %v", path, err)
} }
} }
engine, err := scriptorium.NewEngine(exampleConfig("./examples/schemas")) engine, err := scriptorium.NewEngine(contractConfig(frameworkSchemaDir))
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
return engine return engine
} }
func newExampleEngineWithOptions(t *testing.T, schemaDir string, opts ...scriptorium.Option) *scriptorium.Engine { func newContractEngineWithOptions(t *testing.T, schemaDir string, opts ...scriptorium.Option) *scriptorium.Engine {
t.Helper() t.Helper()
engine, err := scriptorium.NewEngine(exampleConfig(schemaDir), opts...) engine, err := scriptorium.NewEngine(contractConfig(schemaDir), opts...)
if err != nil { if err != nil {
t.Fatalf("expected engine construction to succeed, got %v", err) t.Fatalf("expected engine construction to succeed, got %v", err)
} }
return engine return engine
} }
func exampleConfig(schemaDir string) scriptorium.Config { func contractConfig(schemaDir string) scriptorium.Config {
return scriptorium.Config{ return scriptorium.Config{
PromptDir: "./examples/prompts", PromptDir: frameworkPromptDir,
ProfileDir: "./examples/profiles", ProfileDir: frameworkProfileDir,
SchemaDir: schemaDir, SchemaDir: schemaDir,
} }
} }
@@ -1962,7 +2024,7 @@ func publicStructuredPromptFS(id string, schemaPath string) fstest.MapFS {
return fstest.MapFS{ return fstest.MapFS{
"prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`id: ` + id + ` "prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`id: ` + id + `
version: "1.0.0" version: "1.0.0"
default_profile: local-fast default_profile: contract-fast
inputs: inputs:
- name: transcript - name: transcript
required: true required: true

View File

@@ -1,127 +0,0 @@
package usecase
import (
"context"
"path/filepath"
"testing"
"gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
"gitea.maximumdirect.net/eric/scriptorium/internal/profile"
"gitea.maximumdirect.net/eric/scriptorium/internal/prompt"
"gitea.maximumdirect.net/eric/scriptorium/internal/promptdef"
"gitea.maximumdirect.net/eric/scriptorium/internal/validate"
)
type integrationLLM struct{}
func (f *integrationLLM) Generate(ctx context.Context, req domain.GenerateRequest) (*domain.GenerateResponse, error) {
lastIntegrationRequest = req
return &domain.GenerateResponse{
Content: `{"summary":"Party discovered a captive scout beneath the tower.","events":[{"title":"Scout found in cellar","type":"discovery","notes":"Scout requested rescue from goblin raiders."}]}`,
Usage: domain.TokenUsage{
PromptTokens: 42,
CompletionTokens: 36,
TotalTokens: 78,
},
}, nil
}
var lastIntegrationRequest domain.GenerateRequest
func TestRunnerIntegrationWithPromptAndProfileFixturesAndValidation(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("failed to resolve repo root: %v", err)
}
promptsDir := filepath.Join(root, "examples", "prompts")
profilesDir := filepath.Join(root, "examples", "profiles")
schemasDir := filepath.Join(root, "examples", "schemas")
fixturesDir := filepath.Join(root, "examples", "fixtures")
t.Setenv("SCRIPTORIUM_API_KEY", "test-key")
runner := NewRunner(
promptdef.NewFilesystemRepository(promptsDir),
profile.NewFilesystemRepository(profilesDir),
artifact.NewCompositeReader(),
prompt.NewGoRenderer(),
&integrationLLM{},
validate.NewStandardValidator(schemasDir),
)
res, err := runner.Run(context.Background(), domain.RunRequest{
PromptID: "generic.structured_events",
Inputs: map[string]domain.ArtifactRef{
"transcript": {
Type: domain.ArtifactRefFile,
URI: filepath.Join(fixturesDir, "transcript.md"),
},
"glossary": {
Type: domain.ArtifactRefFile,
URI: filepath.Join(fixturesDir, "glossary.yml"),
},
},
})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if res.PromptID != "generic.structured_events" {
t.Fatalf("unexpected prompt id: %q", res.PromptID)
}
if res.SelectedProfileID != "local-quality" {
t.Fatalf("expected selected profile local-quality from prompt default, got %q", res.SelectedProfileID)
}
if res.RunID == "" {
t.Fatal("expected run id")
}
if res.PromptHash == "" {
t.Fatal("expected prompt hash")
}
if res.PromptVersion != "1.0.0" {
t.Fatalf("unexpected prompt version: %q", res.PromptVersion)
}
if res.Validation.Status != domain.ValidationPassed {
t.Fatalf("expected passed validation, got %q", res.Validation.Status)
}
if res.Validation.Mode != domain.ValidationJSONSchema {
t.Fatalf("expected json_schema mode, got %q", res.Validation.Mode)
}
if lastIntegrationRequest.StructuredOutput == nil {
t.Fatal("expected provider-level structured output request for json_schema prompt")
}
if lastIntegrationRequest.StructuredOutput.Type != domain.StructuredOutputJSONSchema {
t.Fatalf("expected structured output type json_schema, got %q", lastIntegrationRequest.StructuredOutput.Type)
}
if lastIntegrationRequest.StructuredOutput.JSONSchema == nil || lastIntegrationRequest.StructuredOutput.JSONSchema.Schema == nil {
t.Fatalf("expected structured output json_schema payload, got %+v", lastIntegrationRequest.StructuredOutput.JSONSchema)
}
if res.Artifact.ContentType != "application/json" {
t.Fatalf("expected application/json output, got %q", res.Artifact.ContentType)
}
if len(res.RawOutput) == 0 {
t.Fatal("expected raw output to be preserved")
}
if res.PromptHash == "" {
t.Fatal("expected non-empty prompt hash")
}
if len(res.InputHashes) != 2 {
t.Fatalf("expected two input hashes, got %d", len(res.InputHashes))
}
if res.InputHashes["transcript"] == "" || res.InputHashes["glossary"] == "" {
t.Fatalf("expected both input hashes to be set, got %#v", res.InputHashes)
}
if res.Usage.TotalTokens != 78 {
t.Fatalf("expected usage from fake llm, got %+v", res.Usage)
}
if res.StartTime.IsZero() || res.EndTime.IsZero() {
t.Fatal("expected start/end timestamps")
}
if res.EndTime.Before(res.StartTime) {
t.Fatalf("expected end >= start, got start=%v end=%v", res.StartTime, res.EndTime)
}
if res.Duration < 0 {
t.Fatalf("expected non-negative duration, got %s", res.Duration)
}
}