Add Today generate command workflow

This commit is contained in:
2026-06-15 14:58:36 +00:00
parent 3d5f71e72d
commit 4a0238909b
5 changed files with 314 additions and 9 deletions

View File

@@ -20,6 +20,7 @@ the managed Markdown report after final metadata is saved.
```text ```text
weatherreporter --help weatherreporter --help
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
@@ -38,15 +39,15 @@ weatherreporter inspect sources [--config PATH] RUN_ID
Implemented `generate` commands write a JSON module snapshot, YAML data package, Implemented `generate` commands write a JSON module snapshot, YAML data package,
preflight artifact, managed Markdown report, and metadata under the configured preflight artifact, managed Markdown report, and metadata under the configured
workspace. `--out` writes an extra Markdown copy for the operator; distributor workspace. `--out` writes an extra Markdown copy for the operator; distributor
notification uses the managed report path, not the extra copy. `generate notification uses the managed report path, not the extra copy. `generate today`,
tomorrow` and `generate hourly` write managed generated-text artifacts, `generate tomorrow`, and `generate hourly` write managed generated-text artifacts,
validate structured text from Scriptorium, and render the managed Markdown validate structured text from Scriptorium, and render the managed Markdown
report from embedded templates. `generate hourly` covers the next six hours in report from embedded templates. `generate hourly` covers the next six hours in
the effective report timezone and does not accept date or event window flags. the effective report timezone and does not accept date or event window flags.
`generate storm` requires explicit event-window bounds with `--start` and `generate storm` requires explicit event-window bounds with `--start` and
`--end`. `--end`.
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook `run morning` generates Today Report and the 3-Day Outlook, plus Weekend Outlook
except on Sunday. `run evening` generates the Tomorrow Report. Batch except on Sunday. `run evening` generates the Tomorrow Report. Batch
runs continue independent reports after a failure, print a JSON summary to runs continue independent reports after a failure, print a JSON summary to
stdout, write compact status lines to stderr, and return nonzero when any report stdout, write compact status lines to stderr, and return nonzero when any report
@@ -70,7 +71,7 @@ They do not fetch weather data or invoke `scriptorium`.
- `--tz NAME`: override configured Weather API timezone for `generate` and `run`. - `--tz NAME`: override configured Weather API timezone for `generate` and `run`.
- `--out PATH`: write an extra Markdown report copy where supported by the `generate` command. - `--out PATH`: write an extra Markdown report copy where supported by the `generate` command.
- `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`. - `--out-dir PATH`: write extra Markdown report copies for `run morning` and `run evening`.
- `--date YYYY-MM-DD`: optional date for `generate daily`; defaults to the current local date in the configured timezone. - `--date YYYY-MM-DD`: optional date for `generate daily` and `generate today`; defaults to the current local date in the configured timezone.
- `--start TIME`: required start time for `generate storm`. - `--start TIME`: required start time for `generate storm`.
- `--end TIME`: required end time for `generate storm`. - `--end TIME`: required end time for `generate storm`.
- `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit. - `--limit N`: maximum records for `inspect reports`; defaults to `20`, and `0` means no limit.
@@ -85,6 +86,7 @@ are no distributor-specific CLI flags.
```sh ```sh
weatherreporter generate tomorrow --out ./tomorrow.md weatherreporter generate tomorrow --out ./tomorrow.md
weatherreporter generate today --out ./today.md
weatherreporter generate hourly weatherreporter generate hourly
weatherreporter generate three-day --out ./three-day.md weatherreporter generate three-day --out ./three-day.md
weatherreporter generate weekend --out ./weekend.md weatherreporter generate weekend --out ./weekend.md

View File

@@ -30,6 +30,7 @@ type ReportKind string
const ( const (
ReportDaily ReportKind = ReportKind(report.CommandNameDaily) ReportDaily ReportKind = ReportKind(report.CommandNameDaily)
ReportToday ReportKind = ReportKind(report.CommandNameToday)
ReportTomorrow ReportKind = ReportKind(report.CommandNameTomorrow) ReportTomorrow ReportKind = ReportKind(report.CommandNameTomorrow)
ReportHourly ReportKind = ReportKind(report.CommandNameHourly) ReportHourly ReportKind = ReportKind(report.CommandNameHourly)
ReportThreeDay ReportKind = ReportKind(report.CommandNameThreeDay) ReportThreeDay ReportKind = ReportKind(report.CommandNameThreeDay)

View File

@@ -597,6 +597,108 @@ func TestGenerateHourlyReportCopiesOutputAndNotifiesManagedReport(t *testing.T)
} }
} }
func TestGenerateTodayReportCopiesOutputAndNotifiesTodayTemplateValues(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
cfg.Notify.Distributor.Enabled = true
cfg.Notify.Distributor.PipelineIDTemplate = "weatherreporter.{report_id}.{artifact_group}"
cfg.Notify.Distributor.BundleIDTemplate = "{artifact_group}.{batch_output_name}.{report_id}"
cfg.Notify.Distributor.IdempotencyKeyTemplate = "{bundle_id}.{run_id}"
cfg.Notify.Distributor.ReportPathTemplates = []string{"{valid_start_date}/{artifact_group}/{batch_output_name}"}
resolved, err := ResolveGenerate(GenerateRequest{
Config: cfg,
Report: ReportToday,
Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
outputPath := filepath.Join(t.TempDir(), "today-copy.md")
notifier := &recordingNotifier{
result: &NotificationResult{
RunID: "distributor-run",
Status: "succeeded",
UploadStatus: "accepted",
PipelineID: "reports",
Report: []byte(`{"actions":[{"action":"replace_older"}]}`),
},
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
structuredRunBody: validTodayGeneratedTextJSON(),
}
result, err := GenerateReport(context.Background(), ReportRequest{
Config: cfg,
Resolved: resolved,
OutputPath: outputPath,
Renderer: renderer,
Notifier: notifier,
})
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
}
if result.OutputPath != outputPath {
t.Fatalf("OutputPath = %q, want requested copy %q", result.OutputPath, outputPath)
}
assertPathsExist(t, result.ReportPath, outputPath, result.NotificationPath)
reportData, err := os.ReadFile(result.ReportPath)
if err != nil {
t.Fatalf("read managed report: %v", err)
}
copyData, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("read output copy: %v", err)
}
if string(copyData) != string(reportData) {
t.Fatalf("output copy differs from managed report")
}
if len(notifier.requests) != 1 {
t.Fatalf("notification requests = %d, want 1", len(notifier.requests))
}
req := notifier.requests[0]
if req.ReportID != report.Today {
t.Fatalf("notification ReportID = %q, want today", req.ReportID)
}
if req.ReportPath != result.ReportPath {
t.Fatalf("notification ReportPath = %q, want managed report path %q", req.ReportPath, result.ReportPath)
}
if req.ReportPath == outputPath {
t.Fatalf("notification used output copy %q, want managed report path", outputPath)
}
if req.PipelineID != "weatherreporter.today.today" {
t.Fatalf("PipelineID = %q, want Today report and artifact values", req.PipelineID)
}
if req.BundleID != "today.today.md.today" {
t.Fatalf("BundleID = %q, want Today artifact group, output name, and report id", req.BundleID)
}
wantBundlePaths := []string{"2026-05-29/today/today.md"}
if strings.Join(req.BundlePaths, "\n") != strings.Join(wantBundlePaths, "\n") {
t.Fatalf("BundlePaths = %#v, want %#v", req.BundlePaths, wantBundlePaths)
}
notificationData, err := os.ReadFile(result.NotificationPath)
if err != nil {
t.Fatalf("read notification artifact: %v", err)
}
var notificationArtifact state.DistributorNotificationArtifact
if err := json.Unmarshal(notificationData, &notificationArtifact); err != nil {
t.Fatalf("decode notification artifact: %v", err)
}
if notificationArtifact.ReportID != report.Today ||
notificationArtifact.PipelineID != "weatherreporter.today.today" ||
notificationArtifact.BundleID != "today.today.md.today" ||
notificationArtifact.SourcePath != result.ReportPath ||
strings.Join(notificationArtifact.BundlePaths, "\n") != strings.Join(wantBundlePaths, "\n") ||
notificationArtifact.RunStatus == nil ||
!strings.Contains(string(notificationArtifact.RunStatus.Report), "replace_older") {
t.Fatalf("notification artifact = %#v, want Today managed-source notification", notificationArtifact)
}
}
func TestGenerateHourlyReportNotificationFailureFailsReport(t *testing.T) { func TestGenerateHourlyReportNotificationFailureFailsReport(t *testing.T) {
cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t) cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t)
notifier.err = errors.New("upload rejected") notifier.err = errors.New("upload rejected")
@@ -1350,7 +1452,7 @@ func TestGenerateTodayReportUsesTodayIdentityAndRecentChanges(t *testing.T) {
} }
priorResolved, err := ResolveGenerate(GenerateRequest{ priorResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg, Config: cfg,
Report: ReportKind(report.CommandNameToday), Report: ReportToday,
Date: mustParse("2026-05-29T12:00:00-05:00"), Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T04:00:00-05:00")) }, mustParse("2026-05-29T04:00:00-05:00"))
if err != nil { if err != nil {
@@ -1360,7 +1462,7 @@ func TestGenerateTodayReportUsesTodayIdentityAndRecentChanges(t *testing.T) {
currentResolved, err := ResolveGenerate(GenerateRequest{ currentResolved, err := ResolveGenerate(GenerateRequest{
Config: cfg, Config: cfg,
Report: ReportKind(report.CommandNameToday), Report: ReportToday,
Date: mustParse("2026-05-29T12:00:00-05:00"), Date: mustParse("2026-05-29T12:00:00-05:00"),
}, mustParse("2026-05-29T05:00:00-05:00")) }, mustParse("2026-05-29T05:00:00-05:00"))
if err != nil { if err != nil {
@@ -2302,6 +2404,49 @@ func TestRunBatchUsesOutputDirectory(t *testing.T) {
} }
} }
func TestRunBatchMorningUsesTodayOutputName(t *testing.T) {
server := dailyBundleServer(t)
cfg := config.Defaults()
cfg.WeatherAPI.BaseURL = server.URL + "/"
cfg.WeatherAPI.Timezone = "America/Chicago"
cfg.Workspace.Root = t.TempDir()
outputDir := filepath.Join(t.TempDir(), "reports")
result, err := RunBatchDetailed(context.Background(), BatchRequest{
Config: cfg,
Batch: BatchMorning,
Now: mustParse("2026-05-29T05:00:00-05:00"),
OutputDir: outputDir,
Renderer: &selectiveRenderer{runBody: "# Batch Report\n"},
})
if err != nil {
t.Fatalf("RunBatchDetailed() error = %v", err)
}
if result.Failed != 0 || len(result.Reports) != 3 {
t.Fatalf("summary = %#v, want successful morning batch", result)
}
var todayItem *BatchReportResult
for i := range result.Reports {
if result.Reports[i].ReportID == report.Today {
todayItem = &result.Reports[i]
break
}
}
if todayItem == nil {
t.Fatalf("reports = %#v, want Today item", result.Reports)
}
want := filepath.Join(outputDir, "today.md")
if todayItem.OutputPath != want {
t.Fatalf("Today OutputPath = %q, want %q", todayItem.OutputPath, want)
}
if _, err := os.Stat(want); err != nil {
t.Fatalf("expected Today output copy %q: %v", want, err)
}
if strings.Contains(todayItem.ReportPath, outputDir) {
t.Fatalf("Today ReportPath = %q, want managed report path separate from output copy", todayItem.ReportPath)
}
}
func TestBatchOutputPathUsesHourlyOutputName(t *testing.T) { func TestBatchOutputPathUsesHourlyOutputName(t *testing.T) {
definition, err := report.DefaultRegistry().Lookup(report.Hourly) definition, err := report.DefaultRegistry().Lookup(report.Hourly)
if err != nil { if err != nil {

View File

@@ -18,6 +18,7 @@ const helpText = `weatherreporter prepares weather reports from normalized forec
Usage: Usage:
weatherreporter --help weatherreporter --help
weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD] weatherreporter generate daily [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
weatherreporter generate today [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--date YYYY-MM-DD]
weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] weatherreporter generate three-day [--config PATH] [--units VALUE] [--tz NAME] [--out PATH]
@@ -216,7 +217,7 @@ func (r Runner) resolveGenerate(args []string) (app.GenerateRequest, error) {
} }
switch reportKind { switch reportKind {
case app.ReportDaily: case app.ReportDaily, app.ReportToday:
if opts.Date == "" { if opts.Date == "" {
req.Date = timeutil.LocalDate(r.Clock.Now(), location) req.Date = timeutil.LocalDate(r.Clock.Now(), location)
} else { } else {
@@ -278,7 +279,7 @@ func parseGenerateFlags(report app.ReportKind, args []string) (generateOptions,
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
opts := generateOptions{} opts := generateOptions{}
addCommonFlags(fs, &opts.commonOptions, true) addCommonFlags(fs, &opts.commonOptions, true)
if report == app.ReportDaily { if report == app.ReportDaily || report == app.ReportToday {
fs.StringVar(&opts.Date, "date", "", "report date in YYYY-MM-DD") fs.StringVar(&opts.Date, "date", "", "report date in YYYY-MM-DD")
} }
if report == app.ReportStorm { if report == app.ReportStorm {

View File

@@ -26,6 +26,9 @@ func TestRunHelpLongFlag(t *testing.T) {
if !strings.Contains(output.stdout, "generate daily") { if !strings.Contains(output.stdout, "generate daily") {
t.Fatalf("help output missing generate command:\n%s", output.stdout) t.Fatalf("help output missing generate command:\n%s", output.stdout)
} }
if !strings.Contains(output.stdout, "generate today") {
t.Fatalf("help output missing today generate command:\n%s", output.stdout)
}
if !strings.Contains(output.stdout, "weatherreporter generate hourly") { if !strings.Contains(output.stdout, "weatherreporter generate hourly") {
t.Fatalf("help output missing hourly generate command:\n%s", output.stdout) t.Fatalf("help output missing hourly generate command:\n%s", output.stdout)
} }
@@ -570,6 +573,66 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
} }
} }
func TestRunGenerateTodayWritesGeneratedTextReport(t *testing.T) {
server := dailyServer(t)
tempDir := t.TempDir()
scriptoriumPath := writeStructuredOutputScriptorium(t, tempDir)
workspaceRoot := filepath.Join(tempDir, "workspace")
configPath := writeTestConfig(t, server, scriptoriumPath, workspaceRoot)
outPath := filepath.Join(tempDir, "today.md")
var stdout bytes.Buffer
var stderr bytes.Buffer
runner := Runner{Clock: fixedClock()}
err := runner.Run(context.Background(), []string{
"generate", "today",
"--config", configPath,
"--date", "2026-05-29",
"--out", outPath,
}, &stdout, &stderr)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
report, err := os.ReadFile(outPath)
if err != nil {
t.Fatalf("read report: %v", err)
}
for _, want := range []string{
"# Today's Weather",
"Today starts with showers before improving.",
"Morning showers should taper as drier air arrives.",
} {
if !strings.Contains(string(report), want) {
t.Fatalf("today report output missing %q:\n%s", want, string(report))
}
}
dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "today", "2026-05-29", "*.data_package.yaml")
dataPackage, err := os.ReadFile(dataPackagePath)
if err != nil {
t.Fatalf("read managed data package: %v", err)
}
if !strings.Contains(string(dataPackage), "id: today") ||
!strings.Contains(string(dataPackage), "prompt_id: weather.today_generated_text") ||
!strings.Contains(string(dataPackage), "today_planning:") {
t.Fatalf("data package output missing Today content:\n%s", string(dataPackage))
}
dailyPackages, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml"))
if err != nil {
t.Fatalf("glob daily packages: %v", err)
}
if len(dailyPackages) != 0 {
t.Fatalf("daily packages = %#v, want generate today to stay separate", dailyPackages)
}
rawGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "today", "2026-05-29", "*.generated_text.raw.json")
validatedGeneratedTextPath := oneArtifact(t, workspaceRoot, "snapshots", "today", "2026-05-29", "*.generated_text.json")
renderContextPath := oneArtifact(t, workspaceRoot, "snapshots", "today", "2026-05-29", "*.render_context.json")
managedReportPath := oneArtifact(t, workspaceRoot, "reports", "today", "*.md")
assertFileContains(t, rawGeneratedTextPath, `"summary": "Today starts with showers before improving."`)
assertFileContains(t, validatedGeneratedTextPath, `"summary":"Today starts with showers before improving."`)
assertFileContains(t, renderContextPath, `"Title": "Today's Weather"`)
assertFileContains(t, managedReportPath, "# Today's Weather")
}
func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) { func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
server := dailyServer(t) server := dailyServer(t)
tempDir := t.TempDir() tempDir := t.TempDir()
@@ -627,6 +690,65 @@ func TestRunGenerateHourlyWritesGeneratedTextReport(t *testing.T) {
assertFileContains(t, managedReportPath, "# Hourly Report") assertFileContains(t, managedReportPath, "# Hourly Report")
} }
func TestRunInspectTodayArtifacts(t *testing.T) {
server := dailyServer(t)
tempDir := t.TempDir()
scriptoriumPath := writeFakeScriptorium(t, tempDir)
configPath := filepath.Join(tempDir, "config.yml")
workspaceRoot := filepath.Join(tempDir, "workspace")
configBody := "weather_api:\n base_url: " + server.URL + "/\n timezone: America/Chicago\nscriptorium:\n binary: " + scriptoriumPath + "\nworkspace:\n root: " + workspaceRoot + "\n"
if err := os.WriteFile(configPath, []byte(configBody), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
runner := Runner{Clock: fixedClock()}
var stdout bytes.Buffer
var stderr bytes.Buffer
err := runner.Run(context.Background(), []string{
"generate", "today",
"--config", configPath,
"--date", "2026-05-29",
}, &stdout, &stderr)
if err != nil {
t.Fatalf("Run(generate) error = %v", err)
}
dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "today", "2026-05-29", "*.data_package.yaml"))
if err != nil {
t.Fatalf("glob data package: %v", err)
}
if len(dataPackageMatches) != 1 {
t.Fatalf("data package files = %#v, want one", dataPackageMatches)
}
runID := strings.TrimSuffix(filepath.Base(dataPackageMatches[0]), ".data_package.yaml")
stdout.Reset()
err = runner.Run(context.Background(), []string{"inspect", "reports", "--config", configPath, "--limit", "1"}, &stdout, &stderr)
if err != nil {
t.Fatalf("Run(inspect reports) error = %v", err)
}
if !strings.Contains(stdout.String(), runID) || !strings.Contains(stdout.String(), `"reportId": "today"`) {
t.Fatalf("inspect reports output missing Today run:\n%s", stdout.String())
}
var sourcesOutput string
for _, command := range []string{"metadata", "modules", "data-package", "sources"} {
stdout.Reset()
err = runner.Run(context.Background(), []string{"inspect", command, "--config", configPath, runID}, &stdout, &stderr)
if err != nil {
t.Fatalf("Run(inspect %s) error = %v", command, err)
}
if !strings.Contains(stdout.String(), runID) || !strings.Contains(stdout.String(), "today") {
t.Fatalf("inspect %s output missing Today run id:\n%s", command, stdout.String())
}
if command == "sources" {
sourcesOutput = stdout.String()
}
}
if !strings.Contains(sourcesOutput, `"name": "narrative"`) || strings.Contains(sourcesOutput, `"name": "daily"`) {
t.Fatalf("inspect sources output missing narrative source or has unexpected daily source:\n%s", sourcesOutput)
}
}
func TestRunInspectGeneratedArtifacts(t *testing.T) { func TestRunInspectGeneratedArtifacts(t *testing.T) {
server := dailyServer(t) server := dailyServer(t)
tempDir := t.TempDir() tempDir := t.TempDir()
@@ -753,6 +875,7 @@ func TestResolveGenerateCommands(t *testing.T) {
want app.ReportKind want app.ReportKind
}{ }{
{name: "daily", args: []string{"daily", "--date", "2026-05-29"}, want: app.ReportDaily}, {name: "daily", args: []string{"daily", "--date", "2026-05-29"}, want: app.ReportDaily},
{name: "today", args: []string{"today", "--date", "2026-05-29"}, want: app.ReportToday},
{name: "tomorrow", args: []string{"tomorrow"}, want: app.ReportTomorrow}, {name: "tomorrow", args: []string{"tomorrow"}, want: app.ReportTomorrow},
{name: "hourly", args: []string{"hourly"}, want: app.ReportHourly}, {name: "hourly", args: []string{"hourly"}, want: app.ReportHourly},
{name: "three-day", args: []string{"three-day"}, want: app.ReportThreeDay}, {name: "three-day", args: []string{"three-day"}, want: app.ReportThreeDay},
@@ -778,7 +901,7 @@ func TestResolveGenerateSupportsEveryReportCommandName(t *testing.T) {
for _, name := range report.CommandNames() { for _, name := range report.CommandNames() {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
args := []string{name} args := []string{name}
if name == report.CommandNameDaily { if name == report.CommandNameDaily || name == report.CommandNameToday {
args = append(args, "--date", "2026-05-29") args = append(args, "--date", "2026-05-29")
} }
if name == report.CommandNameStorm { if name == report.CommandNameStorm {
@@ -865,6 +988,39 @@ func TestResolveGenerateDailyDefaultsDateInConfiguredTimezone(t *testing.T) {
} }
} }
func TestResolveGenerateTodayDate(t *testing.T) {
runner := Runner{Clock: fixedClock()}
defaultReq, err := runner.resolveGenerate([]string{"today"})
if err != nil {
t.Fatalf("resolveGenerate(default) error = %v", err)
}
if defaultReq.Report != app.ReportToday {
t.Fatalf("Report = %q, want today", defaultReq.Report)
}
if got := defaultReq.Date.Format(timeutil.DateLayout); got != "2026-05-29" {
t.Fatalf("default Date = %s, want 2026-05-29", got)
}
explicitReq, err := runner.resolveGenerate([]string{"today", "--date", "2026-05-30", "--tz", "UTC"})
if err != nil {
t.Fatalf("resolveGenerate(explicit) error = %v", err)
}
if got := explicitReq.Date.Format(timeutil.DateLayout); got != "2026-05-30" {
t.Fatalf("explicit Date = %s, want 2026-05-30", got)
}
resolved, err := app.ResolveGenerate(explicitReq, explicitReq.Now)
if err != nil {
t.Fatalf("ResolveGenerate() error = %v", err)
}
if resolved.Definition.ID != report.Today {
t.Fatalf("resolved ID = %q, want today", resolved.Definition.ID)
}
if got := resolved.ValidPeriod.Start.Format(time.RFC3339); got != "2026-05-30T00:00:00Z" {
t.Fatalf("valid period start = %s, want explicit UTC date", got)
}
}
func TestResolveGenerateAppliesSharedFlags(t *testing.T) { func TestResolveGenerateAppliesSharedFlags(t *testing.T) {
runner := Runner{Clock: fixedClock()} runner := Runner{Clock: fixedClock()}