package scriptorium import ( "context" "reflect" "strings" "testing" "time" ) func TestRenderConstructsCommand(t *testing.T) { commands := &fakeCommands{result: CommandResult{Stdout: []byte(`{"ok":true}`)}} runner := Runner{ Binary: "/usr/local/bin/scriptorium", ConfigPath: "/etc/scriptorium.yml", Profile: "weather", Timeout: time.Minute, Commands: commands, } result, err := runner.Render(context.Background(), RenderRequest{ PromptID: "weather.daily_report", DataPackagePath: "/tmp/data_package.yaml", }) if err != nil { t.Fatalf("Render() error = %v", err) } wantArgs := []string{ "render", "--config", "/etc/scriptorium.yml", "--profile", "weather", "--prompt", "weather.daily_report", "--input", "data_package=/tmp/data_package.yaml", "--format", "json", } if commands.name != "/usr/local/bin/scriptorium" { t.Fatalf("command name = %q, want custom binary", commands.name) } if !reflect.DeepEqual(commands.args, wantArgs) { t.Fatalf("args = %#v, want %#v", commands.args, wantArgs) } if !reflect.DeepEqual(result.Command, append([]string{"/usr/local/bin/scriptorium"}, wantArgs...)) { t.Fatalf("result command = %#v, want full argv", result.Command) } } func TestRenderReturnsResultForNonzeroExit(t *testing.T) { runner := Runner{ Commands: &fakeCommands{ result: CommandResult{ Stderr: []byte("missing input"), ExitCode: 1, }, }, } result, err := runner.Render(context.Background(), RenderRequest{ PromptID: "weather.daily_report", DataPackagePath: "/tmp/data_package.yaml", }) if err == nil { t.Fatal("Render() error = nil, want nonzero exit error") } if result == nil { t.Fatal("Render() result = nil, want captured result") } if result.ExitCode != 1 { t.Fatalf("ExitCode = %d, want 1", result.ExitCode) } if !strings.Contains(err.Error(), "missing input") { t.Fatalf("error = %q, want stderr context", err.Error()) } } func TestRunConstructsCommand(t *testing.T) { commands := &fakeCommands{result: CommandResult{Stderr: []byte("wrote report")}} runner := Runner{ Binary: "/usr/local/bin/scriptorium", ConfigPath: "/etc/scriptorium.yml", Profile: "weather", Timeout: 45 * time.Second, Commands: commands, } result, err := runner.Run(context.Background(), RunRequest{ PromptID: "weather.daily_report", DataPackagePath: "/tmp/data_package.yaml", OutputPath: "/tmp/daily.md", }) if err != nil { t.Fatalf("Run() error = %v", err) } wantArgs := []string{ "run", "--config", "/etc/scriptorium.yml", "--profile", "weather", "--prompt", "weather.daily_report", "--input", "data_package=/tmp/data_package.yaml", "--out", "/tmp/daily.md", } if commands.name != "/usr/local/bin/scriptorium" { t.Fatalf("command name = %q, want custom binary", commands.name) } if !reflect.DeepEqual(commands.args, wantArgs) { t.Fatalf("args = %#v, want %#v", commands.args, wantArgs) } if commands.timeout != 45*time.Second { t.Fatalf("timeout = %s, want 45s", commands.timeout) } if !reflect.DeepEqual(result.Command, append([]string{"/usr/local/bin/scriptorium"}, wantArgs...)) { t.Fatalf("result command = %#v, want full argv", result.Command) } if result.OutputPath != "/tmp/daily.md" { t.Fatalf("OutputPath = %q, want /tmp/daily.md", result.OutputPath) } } func TestRunReturnsResultForValidationExit(t *testing.T) { runner := Runner{ Commands: &fakeCommands{ result: CommandResult{ Stdout: []byte("# Daily Report\n"), Stderr: []byte("validation failed"), ExitCode: 2, }, }, } result, err := runner.Run(context.Background(), RunRequest{ PromptID: "weather.daily_report", DataPackagePath: "/tmp/data_package.yaml", OutputPath: "/tmp/daily.md", }) if err == nil { t.Fatal("Run() error = nil, want nonzero exit error") } if result == nil { t.Fatal("Run() result = nil, want captured result") } if result.ExitCode != 2 { t.Fatalf("ExitCode = %d, want 2", result.ExitCode) } if !strings.Contains(err.Error(), "validation failed") { t.Fatalf("error = %q, want stderr context", err.Error()) } } func TestStructuredRunConstructsCommandWithoutSchemaFlags(t *testing.T) { commands := &fakeCommands{result: CommandResult{ Stdout: []byte(`{"summary":"ok"}`), Stderr: []byte("wrote generated text"), StdoutTruncated: true, }} runner := Runner{ Binary: "/usr/local/bin/scriptorium", ConfigPath: "/etc/scriptorium.yml", Profile: "weather", Timeout: 30 * time.Second, Commands: commands, } result, err := runner.StructuredRun(context.Background(), StructuredRunRequest{ PromptID: "weather.hourly_generated_text", DataPackagePath: "/tmp/hourly.data_package.yaml", OutputPath: "/tmp/hourly.generated_text.raw.json", }) if err != nil { t.Fatalf("StructuredRun() error = %v", err) } wantArgs := []string{ "run", "--config", "/etc/scriptorium.yml", "--profile", "weather", "--prompt", "weather.hourly_generated_text", "--input", "data_package=/tmp/hourly.data_package.yaml", "--out", "/tmp/hourly.generated_text.raw.json", } if commands.name != "/usr/local/bin/scriptorium" { t.Fatalf("command name = %q, want custom binary", commands.name) } if !reflect.DeepEqual(commands.args, wantArgs) { t.Fatalf("args = %#v, want %#v", commands.args, wantArgs) } for _, disallowed := range []string{"--format", "--schema", "--schema-path", "--json-schema"} { if containsArg(commands.args, disallowed) { t.Fatalf("args = %#v, should not include %q", commands.args, disallowed) } } if commands.timeout != 30*time.Second { t.Fatalf("timeout = %s, want 30s", commands.timeout) } if !reflect.DeepEqual(result.Command, append([]string{"/usr/local/bin/scriptorium"}, wantArgs...)) { t.Fatalf("result command = %#v, want full argv", result.Command) } if result.Stdout != `{"summary":"ok"}` || result.Stderr != "wrote generated text" || !result.StdoutTruncated { t.Fatalf("result = %#v, want captured output and truncation flags", result) } if result.OutputPath != "/tmp/hourly.generated_text.raw.json" { t.Fatalf("OutputPath = %q, want generated text raw path", result.OutputPath) } } func TestStructuredRunReturnsResultForNonzeroExit(t *testing.T) { runner := Runner{ Commands: &fakeCommands{ result: CommandResult{ Stdout: []byte(`{"summary":"partial"}`), Stderr: []byte("structured output failed"), ExitCode: 3, }, }, } result, err := runner.StructuredRun(context.Background(), StructuredRunRequest{ PromptID: "weather.hourly_generated_text", DataPackagePath: "/tmp/hourly.data_package.yaml", OutputPath: "/tmp/hourly.generated_text.raw.json", }) if err == nil { t.Fatal("StructuredRun() error = nil, want nonzero exit error") } if result == nil { t.Fatal("StructuredRun() result = nil, want captured result") } if result.ExitCode != 3 { t.Fatalf("ExitCode = %d, want 3", result.ExitCode) } if result.Stdout != `{"summary":"partial"}` || result.OutputPath != "/tmp/hourly.generated_text.raw.json" { t.Fatalf("result = %#v, want captured result fields", result) } if !strings.Contains(err.Error(), "structured output failed") { t.Fatalf("error = %q, want stderr context", err.Error()) } } func TestStructuredRunValidatesRequiredFieldsBeforeExecution(t *testing.T) { tests := []struct { name string req StructuredRunRequest want string }{ { name: "prompt id", req: StructuredRunRequest{ DataPackagePath: "/tmp/hourly.data_package.yaml", OutputPath: "/tmp/hourly.generated_text.raw.json", }, want: "prompt id is required", }, { name: "data package path", req: StructuredRunRequest{ PromptID: "weather.hourly_generated_text", OutputPath: "/tmp/hourly.generated_text.raw.json", }, want: "data package path is required", }, { name: "output path", req: StructuredRunRequest{ PromptID: "weather.hourly_generated_text", DataPackagePath: "/tmp/hourly.data_package.yaml", }, want: "output path is required", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { commands := &fakeCommands{} runner := Runner{Commands: commands} result, err := runner.StructuredRun(context.Background(), test.req) if err == nil { t.Fatal("StructuredRun() error = nil, want validation error") } if result != nil { t.Fatalf("StructuredRun() result = %#v, want nil", result) } if !strings.Contains(err.Error(), test.want) { t.Fatalf("StructuredRun() error = %v, want %q", err, test.want) } if commands.calls != 0 { t.Fatalf("commands calls = %d, want no subprocess execution", commands.calls) } }) } } type fakeCommands struct { name string args []string timeout time.Duration result CommandResult err error calls int } func (f *fakeCommands) Run(_ context.Context, name string, args []string, timeout time.Duration) (CommandResult, error) { f.calls++ f.name = name f.args = append([]string{}, args...) f.timeout = timeout return f.result, f.err } func containsArg(args []string, want string) bool { for _, arg := range args { if arg == want { return true } } return false }