package cli import ( "bytes" "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "gitea.maximumdirect.net/eric/weatherreporter/internal/app" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" ) func TestGenerateInputFailuresDoNotConstructOrExecute(t *testing.T) { configPath := actionConfigPath(t) for _, tt := range []struct { name string args []string wantErr string }{ {name: "MissingReport", args: nil, wantErr: "generate requires a report name"}, {name: "UnknownReport", args: []string{"unknown"}, wantErr: "unknown generate report"}, {name: "MissingDailyDate", args: []string{"daily", "--config", filepath.Join(t.TempDir(), "missing.yml")}, wantErr: "generate daily requires --date YYYY-MM-DD"}, {name: "MalformedDailyDate", args: []string{"daily", "--date", "not-a-date", "--config", configPath}}, {name: "MalformedTodayDate", args: []string{"today", "--date", "not-a-date", "--config", configPath}}, {name: "UnexpectedArgument", args: []string{"today", "extra"}}, {name: "UnsupportedFlag", args: []string{"today", "--out-dir", "reports"}}, } { t.Run(tt.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 }, generateDetailed: func(context.Context, app.GenerateRequest) (*app.ReportResult, error) { applicationCalls++ return nil, nil }, } var stdout, stderr bytes.Buffer err := runner.Run(context.Background(), append([]string{"generate"}, tt.args...), &stdout, &stderr) if err == nil { t.Fatal("Run() error = nil") } if tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("Run() error = %q, want %q", err, tt.wantErr) } if factoryCalls != 0 || applicationCalls != 0 || stdout.Len() != 0 || stderr.Len() != 0 { t.Fatalf("factory/application calls/output = %d/%d/%q/%q", factoryCalls, applicationCalls, stdout.String(), stderr.String()) } }) } } func TestGenerateCommandProjectsActionResults(t *testing.T) { configPath := actionConfigPath(t) failure := errors.New("prompt execution failed") for _, tt := range []struct { name string quiet bool actionErr error }{ {name: "Success"}, {name: "Failure", actionErr: failure}, {name: "QuietFailure", quiet: true, actionErr: failure}, } { t.Run(tt.name, func(t *testing.T) { factoryCalls, applicationCalls := 0, 0 executor := &factoryExecutor{} 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 executor, nil }, generateDetailed: func(_ context.Context, req app.GenerateRequest) (*app.ReportResult, error) { applicationCalls++ if req.Executor != executor { t.Fatal("generate request did not receive the constructed executor") } return generatedReportResult(), tt.actionErr }, } args := []string{"generate", "daily", "--date", "2026-05-29", "--config", configPath} if tt.quiet { args = append(args, "--quiet") } var stdout, stderr bytes.Buffer err := runner.Run(context.Background(), args, &stdout, &stderr) if !errors.Is(err, tt.actionErr) || factoryCalls != 1 || applicationCalls != 1 || stderr.Len() != 0 { t.Fatalf("error/calls/stderr = %v/%d/%d/%q", err, factoryCalls, applicationCalls, stderr.String()) } if tt.quiet { if stdout.Len() != 0 { t.Fatalf("quiet stdout = %q, want empty", stdout.String()) } return } var summary generateSummary if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil { t.Fatalf("decode summary: %v", err) } wantStatus, wantError := summaryStatusSucceeded, "" if tt.actionErr != nil { wantStatus, wantError = summaryStatusFailed, tt.actionErr.Error() } if summary.Command != commandGenerate || summary.Status != wantStatus || summary.Error != wantError || summary.OutputPath != "/reports/daily.md" { t.Fatalf("summary = %#v", summary) } }) } } func actionConfigPath(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "config.yml") if err := os.WriteFile(path, []byte("weather_api:\n base_url: https://weather.api.example.com/\n"), 0o600); err != nil { t.Fatal(err) } return path } func generatedReportResult() *app.ReportResult { generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC) return &app.ReportResult{ ReportID: report.Daily, ReportName: "Daily Report", PromptID: "weather.daily_generated_text", PromptVersion: "2.0.0", RunID: "daily-20260529", GeneratedAt: generatedAt, Timezone: "America/Chicago", ValidPeriod: timeutil.Period{Start: generatedAt, End: generatedAt.Add(24 * time.Hour)}, ProfileID: "weather-light", BackendID: "local", ModelName: "weather-model", ValidationStatus: promptexec.ValidationPassed, OutputPath: "/reports/daily.md", } }