142 lines
5.2 KiB
Go
142 lines
5.2 KiB
Go
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",
|
|
}
|
|
}
|