209 lines
8.5 KiB
Go
209 lines
8.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
|
)
|
|
|
|
func TestParseRunFlagsAcceptsPromptDebugDirectory(t *testing.T) {
|
|
opts, err := parseRunFlags([]string{"--llm-debug-dir", "/tmp/prompt-debug"})
|
|
if err != nil || opts.LLMDebugDir != "/tmp/prompt-debug" {
|
|
t.Fatalf("parseRunFlags() = %#v, %v", opts, err)
|
|
}
|
|
}
|
|
|
|
func TestResolveRunActionConstructsOneExecutor(t *testing.T) {
|
|
for _, command := range []string{"morning", "evening"} {
|
|
t.Run(command, func(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
calls := 0
|
|
executor := &factoryExecutor{}
|
|
runner := Runner{
|
|
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
|
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
|
calls++
|
|
return executor, nil
|
|
},
|
|
}
|
|
req, _, err := runner.resolveRunAction([]string{command, "--config", configPath, "--llm-debug-dir", "/tmp/debug"})
|
|
if err != nil || calls != 1 || req.Executor != executor || req.LLMDebugDir != "/tmp/debug" {
|
|
t.Fatalf("resolveRunAction() request/error/calls = %#v/%v/%d", req, err, calls)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveGenerateActionUsesInjectedWorkingDirectoryForOutputOverrides(t *testing.T) {
|
|
workingDir := t.TempDir()
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
const configuredDirectory = "configured/../reports"
|
|
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: "+configuredDirectory+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
absoluteOutput := filepath.Join(t.TempDir(), "daily.md")
|
|
for _, scenario := range []struct {
|
|
name string
|
|
out string
|
|
want string
|
|
}{
|
|
{name: "default", want: ""},
|
|
{name: "relative", out: "reports/daily.md", want: filepath.Join(workingDir, "reports", "daily.md")},
|
|
{name: "absolute", out: absoluteOutput, want: absoluteOutput},
|
|
} {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
runner := Runner{
|
|
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
|
WorkingDir: workingDir,
|
|
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
|
return &factoryExecutor{}, nil
|
|
},
|
|
}
|
|
args := []string{"daily", "--date", "2026-05-29", "--config", configPath}
|
|
if scenario.out != "" {
|
|
args = append(args, "--out", scenario.out)
|
|
}
|
|
req, _, err := runner.resolveGenerateAction(args)
|
|
if err != nil || req.WorkingDir != workingDir || req.OutputPath != scenario.want || req.Config.Output.Directory != configuredDirectory {
|
|
t.Fatalf("resolveGenerateAction() request/error = %#v/%v", req, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveActionsPreserveConfiguredOutputDirectoryWithoutAnOverride(t *testing.T) {
|
|
workingDir := t.TempDir()
|
|
absoluteDirectory := filepath.Join(t.TempDir(), "reports")
|
|
for _, configuredDirectory := range []string{"configured/../reports", absoluteDirectory} {
|
|
t.Run(configuredDirectory, func(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
contents := "weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: " + strconv.Quote(configuredDirectory) + "\n"
|
|
if err := os.WriteFile(configPath, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runner := Runner{
|
|
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
|
WorkingDir: workingDir,
|
|
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
|
return &factoryExecutor{}, nil
|
|
},
|
|
}
|
|
|
|
generateReq, _, generateErr := runner.resolveGenerateAction([]string{"daily", "--date", "2026-05-29", "--config", configPath})
|
|
if generateErr != nil || generateReq.Config.Output.Directory != configuredDirectory || generateReq.OutputPath != "" {
|
|
t.Fatalf("resolveGenerateAction() request/error = %#v/%v", generateReq, generateErr)
|
|
}
|
|
|
|
batchReq, _, batchErr := runner.resolveRunAction([]string{"morning", "--config", configPath})
|
|
if batchErr != nil || batchReq.Config.Output.Directory != configuredDirectory || batchReq.OutputDir != "" {
|
|
t.Fatalf("resolveRunAction() request/error = %#v/%v", batchReq, batchErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveRunActionUsesInjectedWorkingDirectoryForOutputOverrides(t *testing.T) {
|
|
workingDir := t.TempDir()
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
const configuredDirectory = "configured/../reports"
|
|
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: "+configuredDirectory+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
absoluteOutput := filepath.Join(t.TempDir(), "reports")
|
|
for _, scenario := range []struct {
|
|
name string
|
|
out string
|
|
want string
|
|
}{
|
|
{name: "relative", out: "reports/../published", want: filepath.Join(workingDir, "published")},
|
|
{name: "absolute", out: absoluteOutput, want: absoluteOutput},
|
|
} {
|
|
t.Run(scenario.name, func(t *testing.T) {
|
|
runner := Runner{
|
|
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
|
WorkingDir: workingDir,
|
|
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
|
return &factoryExecutor{}, nil
|
|
},
|
|
}
|
|
req, _, err := runner.resolveRunAction([]string{"morning", "--config", configPath, "--out-dir", scenario.out})
|
|
if err != nil || req.WorkingDir != workingDir || req.OutputDir != scenario.want || req.Config.Output.Directory != configuredDirectory {
|
|
t.Fatalf("resolveRunAction() request/error = %#v/%v", req, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunActionReturnsFailureForBatchNotificationFailure(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
if err := os.WriteFile(configPath, []byte("weather_api:\n base_url: https://weather.api.example.com/\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := &app.BatchResult{
|
|
Batch: app.BatchMorning, Total: 2, Succeeded: 2,
|
|
Reports: []app.BatchReportResult{
|
|
{ReportID: "today", Status: "succeeded", OutputPath: "/reports/today.md"},
|
|
{ReportID: "tomorrow", Status: "succeeded", OutputPath: "/reports/tomorrow.md"},
|
|
},
|
|
Notification: &app.BatchNotificationResult{Status: "failed", Error: "distributor unavailable"},
|
|
}
|
|
var stdout, stderr bytes.Buffer
|
|
runner := Runner{
|
|
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)},
|
|
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
|
return &factoryExecutor{}, nil
|
|
},
|
|
runBatchDetailed: func(context.Context, app.BatchRequest) (*app.BatchResult, error) {
|
|
return result, nil
|
|
},
|
|
}
|
|
err := runner.Run(context.Background(), []string{"run", "morning", "--config", configPath}, &stdout, &stderr)
|
|
var batchErr app.BatchError
|
|
if !errors.As(err, &batchErr) || !strings.Contains(err.Error(), "notification failed") {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
var summary batchSummary
|
|
if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil {
|
|
t.Fatalf("decode summary: %v", err)
|
|
}
|
|
if summary.Status != summaryStatusFailed || summary.Total != 2 || summary.Succeeded != 2 || summary.Failed != 0 || summary.Notification == nil || summary.Notification.Status != "failed" {
|
|
t.Fatalf("summary = %#v", summary)
|
|
}
|
|
if !strings.Contains(stderr.String(), `batchNotification status="failed"`) {
|
|
t.Fatalf("stdout/stderr = %q/%q", stdout.String(), stderr.String())
|
|
}
|
|
for _, field := range []string{"notificationStatus", "notificationRunId", "notificationPipelineId", "notificationError"} {
|
|
if strings.Contains(stdout.String(), field) || strings.Contains(stderr.String(), field) {
|
|
t.Fatalf("stdout/stderr includes removed field %q: %q/%q", field, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInspectCommandIsUnknownAndAbsentFromHelp(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
err := (Runner{}).Run(context.Background(), []string{"inspect", "reports"}, &stdout, &stderr)
|
|
if err == nil || err.Error() != `unknown command "inspect"` {
|
|
t.Fatalf("Run(inspect) error = %v", err)
|
|
}
|
|
if err := (Runner{}).Run(context.Background(), []string{"--help"}, &stdout, &stderr); err != nil {
|
|
t.Fatalf("Run(--help) error = %v", err)
|
|
}
|
|
if strings.Contains(stdout.String(), "inspect") {
|
|
t.Fatalf("help contains removed inspect command:\n%s", stdout.String())
|
|
}
|
|
}
|