Add comparison command request parsing
This commit is contained in:
204
internal/cli/comparison_test.go
Normal file
204
internal/cli/comparison_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"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 TestParseComparisonFlagsPreservesProfileOrder(t *testing.T) {
|
||||
opts, err := parseComparisonFlags(app.ReportDaily, []string{
|
||||
"--profile", "weather-light", "--profile=weather-balanced", "--profile", "weather-deep",
|
||||
"--date", "2026-05-29", "--out-dir", "reports", "--replace", "--quiet",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("parseComparisonFlags() error = %v", err)
|
||||
}
|
||||
wantProfiles := []string{"weather-light", "weather-balanced", "weather-deep"}
|
||||
if !reflect.DeepEqual([]string(opts.ProfileIDs), wantProfiles) || opts.Date != "2026-05-29" || opts.OutputDir != "reports" || !opts.Replace || !opts.Quiet {
|
||||
t.Fatalf("options = %#v", opts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComparisonActionBuildsExplicitRequest(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n units: metric\n timezone: America/Chicago\noutput:\n directory: configured-reports\npromptkit:\n profile: configured-default\n")
|
||||
clock := timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 30, 0, 0, time.UTC)}
|
||||
var factoryConfig PromptExecutorConfig
|
||||
factoryCalls := 0
|
||||
executor := &factoryExecutor{}
|
||||
runner := Runner{
|
||||
Clock: clock, WorkingDir: workingDir,
|
||||
ExecutorFactory: func(value PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
factoryCalls++
|
||||
factoryConfig = value
|
||||
return executor, nil
|
||||
},
|
||||
}
|
||||
req, opts, err := runner.resolveComparisonAction([]string{
|
||||
"daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep",
|
||||
"--out-dir", "bundles/../comparison", "--replace", "--llm-debug-dir", "/tmp/debug", "--units", "imperial", "--tz", "America/New_York", "--config", configPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("resolveComparisonAction() error = %v", err)
|
||||
}
|
||||
if factoryCalls != 1 || req.Executor != executor || req.Clock != clock || req.WorkingDir != workingDir || req.OutputDir != filepath.Join(workingDir, "comparison") || !req.Replace || req.LLMDebugDir != "/tmp/debug" {
|
||||
t.Fatalf("request/factory calls = %#v/%#v/%d", req, factoryConfig, factoryCalls)
|
||||
}
|
||||
if got, want := req.ProfileIDs, []string{"weather-light", "weather-deep"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("profile IDs = %#v, want %#v", got, want)
|
||||
}
|
||||
if req.Config.WeatherAPI.Units != "imperial" || req.Config.WeatherAPI.Timezone != "America/New_York" || req.Config.Output.Directory != "configured-reports" || factoryConfig.Profile != "" || opts.Quiet {
|
||||
t.Fatalf("configuration/options/factory config = %#v/%#v/%#v", req.Config, opts, factoryConfig)
|
||||
}
|
||||
location, loadErr := time.LoadLocation("America/New_York")
|
||||
if loadErr != nil || req.Date.Location().String() != location.String() || req.Date.Format("2006-01-02") != "2026-05-29" {
|
||||
t.Fatalf("date/location = %v/%v", req.Date, loadErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComparisonActionMatchesReportDatePolicies(t *testing.T) {
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n timezone: America/Chicago\n")
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
wantDay string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "daily requires date", args: []string{"daily", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
{name: "today uses current local date", args: []string{"today", "--profile", "one", "--profile", "two", "--config", configPath}, wantDay: "2026-05-29"},
|
||||
{name: "today accepts date", args: []string{"today", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantDay: "2026-05-30"},
|
||||
{name: "tomorrow rejects date", args: []string{"tomorrow", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
{name: "hourly rejects date", args: []string{"hourly", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, _, err := runner.resolveComparisonAction(test.args)
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("resolveComparisonAction() error = nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || req.Date.Format("2006-01-02") != test.wantDay {
|
||||
t.Fatalf("request/error = %#v/%v", req, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComparisonActionLeavesConfiguredOutputWithoutOverride(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: configured/../reports\n")
|
||||
req, _, err := comparisonRunner(t, workingDir).resolveComparisonAction([]string{
|
||||
"daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep", "--config", configPath,
|
||||
})
|
||||
if err != nil || req.OutputDir != "" || req.Config.Output.Directory != "configured/../reports" {
|
||||
t.Fatalf("request/error = %#v/%v", req, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisonInputFailuresDoNotConstructOrExecute(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{name: "missing report", args: nil},
|
||||
{name: "unknown report", args: []string{"unknown", "--profile", "one", "--profile", "two"}},
|
||||
{name: "single profile", args: []string{"today", "--profile", "one"}},
|
||||
{name: "blank profile", args: []string{"today", "--profile", "one", "--profile", " \t"}},
|
||||
{name: "duplicate profile", args: []string{"today", "--profile", "one", "--profile", "one"}},
|
||||
{name: "unexpected argument", args: []string{"today", "extra", "--profile", "one", "--profile", "two"}},
|
||||
{name: "unsupported output flag", args: []string{"today", "--out", "report.md", "--profile", "one", "--profile", "two"}},
|
||||
{name: "configuration failure", args: []string{"today", "--profile", "one", "--profile", "two", "--config", filepath.Join(t.TempDir(), "missing.yml")}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
factoryCalls, applicationCalls := 0, 0
|
||||
runner := Runner{
|
||||
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 30, 0, 0, time.UTC)},
|
||||
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
factoryCalls++
|
||||
return &factoryExecutor{}, nil
|
||||
},
|
||||
compareDetailed: func(context.Context, app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
applicationCalls++
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
if _, err := runner.executeComparison(context.Background(), test.args); err == nil {
|
||||
t.Fatal("executeComparison() error = nil")
|
||||
}
|
||||
if factoryCalls != 0 || applicationCalls != 0 {
|
||||
t.Fatalf("factory/application calls = %d/%d", factoryCalls, applicationCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteComparisonUsesOneExecutorAndInjectedApplication(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n")
|
||||
executor := &factoryExecutor{}
|
||||
factoryCalls, applicationCalls := 0, 0
|
||||
var received app.ComparisonRequest
|
||||
wantResult := &app.ComparisonResult{ComparisonID: "comparison_test"}
|
||||
runner := Runner{
|
||||
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 30, 0, 0, time.UTC)}, WorkingDir: workingDir,
|
||||
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) {
|
||||
factoryCalls++
|
||||
return executor, nil
|
||||
},
|
||||
compareDetailed: func(_ context.Context, req app.ComparisonRequest) (*app.ComparisonResult, error) {
|
||||
applicationCalls++
|
||||
received = req
|
||||
return wantResult, errors.New("comparison completed with 1 failed profiles")
|
||||
},
|
||||
}
|
||||
result, err := runner.executeComparison(context.Background(), []string{
|
||||
"daily", "--date", "2026-05-29", "--profile", "weather-light", "--profile", "weather-deep", "--out-dir", "comparison", "--replace", "--config", configPath,
|
||||
})
|
||||
if result != wantResult || err == nil || factoryCalls != 1 || applicationCalls != 1 || received.Executor != executor || received.OutputDir != filepath.Join(workingDir, "comparison") || !received.Replace {
|
||||
t.Fatalf("result/error/calls/request = %#v/%v/%d/%d/%#v", result, err, factoryCalls, applicationCalls, received)
|
||||
}
|
||||
if !reflect.DeepEqual(received.ProfileIDs, []string{"weather-light", "weather-deep"}) {
|
||||
t.Fatalf("profile IDs = %#v", received.ProfileIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareRemainsAbsentFromRootDispatchAndHelp(t *testing.T) {
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
var stdout, stderr bytes.Buffer
|
||||
if err := runner.Run(context.Background(), []string{"compare", "today"}, &stdout, &stderr); err == nil || err.Error() != `unknown command "compare"` {
|
||||
t.Fatalf("Run(compare) error = %v", err)
|
||||
}
|
||||
if err := runner.Run(context.Background(), []string{"--help"}, &stdout, &stderr); err != nil || strings.Contains(stdout.String(), "compare") {
|
||||
t.Fatalf("help/error = %q/%v", stdout.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func comparisonRunner(t *testing.T, workingDir string) Runner {
|
||||
t.Helper()
|
||||
return Runner{
|
||||
Clock: timeutil.FixedClock{Time: time.Date(2026, 5, 29, 8, 30, 0, 0, time.UTC)}, WorkingDir: workingDir,
|
||||
ExecutorFactory: func(PromptExecutorConfig) (promptexec.Executor, error) { return &factoryExecutor{}, nil },
|
||||
}
|
||||
}
|
||||
|
||||
func comparisonConfigPath(t *testing.T, contents string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user