Replace legacy daily report with generated text daily report
This commit is contained in:
@@ -110,7 +110,7 @@ weatherreporter inspect modules 20260529T100000.000000000Z_today
|
||||
weatherreporter inspect data-package 20260529T100000.000000000Z_today
|
||||
weatherreporter inspect prior 20260529T100000.000000000Z_today
|
||||
weatherreporter inspect sources 20260529T100000.000000000Z_today
|
||||
weatherreporter inspect metadata 20260529T100000.000000000Z_daily_today
|
||||
weatherreporter inspect metadata 20260529T100000.000000000Z_daily
|
||||
```
|
||||
|
||||
`inspect reports` lists recent generated runs with artifact paths and source
|
||||
|
||||
@@ -183,12 +183,12 @@ report definitions. Omit a report entry to use its default module order.
|
||||
|
||||
Supported report keys are `daily`, `today`, `tomorrow`, `hourly`,
|
||||
`three_day`, `weekend`, and `storm`. Canonical report IDs and accepted aliases
|
||||
are also valid, including `daily_today`, `three_day_outlook`,
|
||||
`weekend_outlook`, and `storm_report`. Hyphens and underscores are treated
|
||||
equivalently in report keys.
|
||||
are also valid, including `three_day_outlook`, `weekend_outlook`, and
|
||||
`storm_report`. Hyphens and underscores are treated equivalently in report
|
||||
keys. `daily_today` is not a supported report key.
|
||||
|
||||
`reports.today` applies only to the Today Report. `reports.daily` and
|
||||
`reports.daily_today` apply only to the existing Daily Today report.
|
||||
`reports.today` applies only to the Today Report. `reports.daily` applies only
|
||||
to the dated Daily Report.
|
||||
|
||||
Each report entry supports:
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ Outputs:
|
||||
Every registered composition entry has a builder. Unknown or unimplemented
|
||||
module IDs fail validation instead of being skipped.
|
||||
|
||||
Daily Report supports the Daily-style civil-day modules plus `daily_planning`
|
||||
and `hourly_forecast`; those outputs feed the dated Daily GeneratedText prompt
|
||||
package and embedded Markdown template.
|
||||
|
||||
Tomorrow Report supports the Daily-style civil-day modules plus
|
||||
`tomorrow_planning` and `hourly_forecast`; those outputs feed the Tomorrow
|
||||
GeneratedText prompt package and embedded Markdown template.
|
||||
@@ -53,8 +57,8 @@ late-day change-watch fields. It is compatible with `report.Today` only.
|
||||
|
||||
`daily_planning` is a dated Daily deterministic planning stanza with morning
|
||||
readiness, commute/school/workday concerns, and overnight change-watch fields.
|
||||
It is compatible only with the `daily` report ID value. No active default
|
||||
report composition includes it yet.
|
||||
It is compatible only with the `daily` report ID value. The default Daily
|
||||
Report composition includes it.
|
||||
|
||||
Hourly Report supports source and valid-period modules that operate over its
|
||||
rolling six-hour period: `metadata`, `current_conditions`, `hourly_forecast`,
|
||||
|
||||
@@ -49,6 +49,28 @@ The registry recognizes these IDs:
|
||||
Every registered module has a builder. Report composition entries that refer to
|
||||
unknown or unimplemented module IDs fail validation instead of being skipped.
|
||||
|
||||
## Daily Composition
|
||||
|
||||
The default Daily Report module order is:
|
||||
|
||||
1. `metadata`
|
||||
2. `current_conditions`
|
||||
3. `narrative_forecast`
|
||||
4. `derived_daily_summary`
|
||||
5. `derived_daypart_summaries`
|
||||
6. `precip_timing`
|
||||
7. `alert_digest`
|
||||
8. `spc_convective_outlooks`
|
||||
9. `area_forecast_discussion`
|
||||
10. `spc_convective_discussion`
|
||||
11. `weather_story`
|
||||
12. `outdoor_windows`
|
||||
13. `daily_planning`
|
||||
14. `hourly_forecast`
|
||||
|
||||
The embedded Daily template uses selected deterministic fields from these
|
||||
module outputs after GeneratedText validation.
|
||||
|
||||
## Today Composition
|
||||
|
||||
The default Today Report module order is:
|
||||
@@ -98,7 +120,7 @@ module outputs after GeneratedText validation.
|
||||
`daily_planning` emits dated daily planning facts for the `daily` report ID.
|
||||
Its output stanza is also named `daily_planning`. The module is supported only
|
||||
by that report ID and depends on daily summaries for the selected local civil
|
||||
day. No active default report composition includes it yet.
|
||||
day. The default Daily Report composition includes it.
|
||||
|
||||
The output uses this shape:
|
||||
|
||||
|
||||
@@ -68,9 +68,9 @@ Prompt-facing module intervals use local `period_begins` and `period_ends`
|
||||
labels; canonical report metadata and source timestamps remain structured
|
||||
timestamps where applicable.
|
||||
|
||||
Today Report, Tomorrow Report, and Hourly Report module snapshots use the same
|
||||
package schema and categories when converted into prompt input. The default
|
||||
hourly module list places
|
||||
Daily Report, Today Report, Tomorrow Report, and Hourly Report module snapshots
|
||||
use the same package schema and categories when converted into prompt input.
|
||||
The default hourly module list places
|
||||
`precip_timing` under `derived_summaries`, alert and SPC outlooks under
|
||||
`applicable_risk_products`, AFD/SPC discussion/weather story under
|
||||
`narrative_products`, and current/hourly data under `raw_data`. It does not
|
||||
@@ -78,10 +78,11 @@ include civil-day summary stanzas. Generated-text and render context artifacts
|
||||
are produced later in app orchestration and are not part of the YAML data
|
||||
package.
|
||||
|
||||
The default Today and Tomorrow module lists include civil-day summary stanzas,
|
||||
planning stanzas, and `hourly_forecast` in the data package before structured
|
||||
GeneratedText is requested from Scriptorium. Today uses `today_planning`;
|
||||
Tomorrow uses `tomorrow_planning`.
|
||||
The default Daily, Today, and Tomorrow module lists include civil-day summary
|
||||
stanzas, planning stanzas, and `hourly_forecast` in the data package before
|
||||
structured GeneratedText is requested from Scriptorium. Daily uses
|
||||
`daily_planning`, Today uses `today_planning`, and Tomorrow uses
|
||||
`tomorrow_planning`.
|
||||
|
||||
Current categories are:
|
||||
|
||||
|
||||
@@ -29,25 +29,25 @@ Each report definition declares:
|
||||
|
||||
Report-owned helpers map public command names and config keys to report IDs.
|
||||
The generate command names are `daily`, `today`, `tomorrow`, `hourly`,
|
||||
`three-day`, `weekend`, and `storm`. Config keys also accept underscore and
|
||||
legacy descriptive aliases such as `daily_today`, `three_day_outlook`,
|
||||
`three-day`, `weekend`, and `storm`. Config keys also accept selected
|
||||
underscore and descriptive aliases such as `three_day_outlook`,
|
||||
`weekend_outlook`, and `storm_report`.
|
||||
|
||||
`daily` resolves to the existing Daily Today report ID `daily_today`.
|
||||
`today` resolves to the independent Today report ID `today`. `reports.today`
|
||||
is not an alias for `reports.daily` or `reports.daily_today`.
|
||||
`daily` resolves to the dated Daily Report ID `daily`. `today` resolves to the
|
||||
independent Today report ID `today`. `reports.today` is not an alias for
|
||||
`reports.daily`, and `reports.daily_today` is not a supported config key.
|
||||
|
||||
Markdown report definitions use the `scriptorium_markdown` generation mode.
|
||||
Their template and structured-text schema identifiers are empty. Today Report,
|
||||
Tomorrow Report, and Hourly Report declare `generated_text_template`; the app
|
||||
uses their template and schema identifiers to validate generated text and render
|
||||
embedded Markdown templates.
|
||||
Their template and structured-text schema identifiers are empty. Daily Report,
|
||||
Today Report, Tomorrow Report, and Hourly Report declare
|
||||
`generated_text_template`; the app uses their template and schema identifiers
|
||||
to validate generated text and render embedded Markdown templates.
|
||||
|
||||
## Reports
|
||||
|
||||
| Report | ID | Prompt | Generation mode | Artifact group | Batch copy | Prior compatibility |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Daily Today | `daily_today` | `weather.daily_report` | `scriptorium_markdown` | `daily` | `daily.md` | Daily Today |
|
||||
| Daily Report | `daily` | `weather.daily_generated_text` | `generated_text_template` | `daily` | `daily.md` | Daily Report |
|
||||
| Today Report | `today` | `weather.today_generated_text` | `generated_text_template` | `today` | `today.md` | Today Report |
|
||||
| Tomorrow Report | `tomorrow` | `weather.tomorrow_generated_text` | `generated_text_template` | `tomorrow` | `tomorrow.md` | Tomorrow Report |
|
||||
| Hourly Report | `hourly` | `weather.hourly_generated_text` | `generated_text_template` | `hourly` | `hourly.md` | Hourly Report |
|
||||
@@ -59,8 +59,8 @@ All report definitions are eligible for generation.
|
||||
|
||||
## Valid Periods
|
||||
|
||||
- Daily Today covers the selected local civil day, or the current local civil
|
||||
day when no date override is supplied.
|
||||
- Daily Report covers the selected local civil day and requires an explicit
|
||||
date.
|
||||
- Today Report covers the selected local civil day, or the current local civil
|
||||
day when no date override is supplied.
|
||||
- Tomorrow Report covers the next local civil day from generation time.
|
||||
@@ -91,14 +91,13 @@ report override keys.
|
||||
|
||||
The app supplies `weather_api.timezone` as a loaded `time.Location`. Batch
|
||||
output path copying uses batch output names from report definitions. Report
|
||||
module overrides can use short keys such as `today`, `tomorrow`, and `hourly`,
|
||||
canonical report IDs such as `daily_today`, or descriptive names such as
|
||||
`three_day_outlook`.
|
||||
module overrides can use short keys such as `daily`, `today`, `tomorrow`, and
|
||||
`hourly`, or descriptive names such as `three_day_outlook`.
|
||||
|
||||
## Batch Membership
|
||||
|
||||
Morning batches include Today Report, 3-Day Outlook, and Weekend Outlook
|
||||
except on Sunday. Evening batches include Tomorrow Report. Daily Today and
|
||||
except on Sunday. Evening batches include Tomorrow Report. Daily Report and
|
||||
Hourly Report are not part of a scheduled batch.
|
||||
|
||||
## State And App Usage
|
||||
|
||||
@@ -196,7 +196,7 @@ period do not overwrite each other.
|
||||
RunIDs are based on generation time plus report ID:
|
||||
|
||||
```text
|
||||
20260529T100000.123456789Z_daily_today
|
||||
20260529T100000.123456789Z_daily
|
||||
20260529T100000.123456789Z_today
|
||||
```
|
||||
|
||||
|
||||
@@ -1107,7 +1107,7 @@ func recentChanges(ctx context.Context, store state.Store, priorSnapshot *state.
|
||||
PrecipTimingShiftMinutes: cfg.PrecipTimingShiftMinutes,
|
||||
}
|
||||
switch reportID {
|
||||
case report.DailyToday, report.Today, report.Tomorrow:
|
||||
case report.Daily, report.Today, report.Tomorrow:
|
||||
return changes.CompareDaily(previous, current, thresholds)
|
||||
case report.ThreeDay:
|
||||
return changes.CompareThreeDay(previous, current, thresholds)
|
||||
|
||||
@@ -105,6 +105,11 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
ExitCode: 0,
|
||||
OutputPath: "",
|
||||
},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{
|
||||
Command: []string{"scriptorium", "run"},
|
||||
Stderr: "wrote generated text",
|
||||
ExitCode: 0,
|
||||
},
|
||||
runBody: "# Daily Report\n\nRain this morning.\n",
|
||||
}
|
||||
outputPath := filepath.Join(t.TempDir(), "daily.md")
|
||||
@@ -128,25 +133,28 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
if renderer.renderCalls != 1 {
|
||||
t.Fatalf("render calls = %d, want 1", renderer.renderCalls)
|
||||
}
|
||||
if renderer.runCalls != 1 {
|
||||
t.Fatalf("run calls = %d, want 1", renderer.runCalls)
|
||||
if renderer.structuredRunCalls != 1 {
|
||||
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != "weather.daily_report" {
|
||||
t.Fatalf("render PromptID = %q, want weather.daily_report", renderer.renderRequest.PromptID)
|
||||
if renderer.runCalls != 0 {
|
||||
t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
|
||||
}
|
||||
if renderer.renderRequest.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("render PromptID = %q, want weather.daily_generated_text", renderer.renderRequest.PromptID)
|
||||
}
|
||||
if renderer.renderRequest.DataPackagePath != result.DataPackagePath {
|
||||
t.Fatalf("render DataPackagePath = %q, want managed path %q", renderer.renderRequest.DataPackagePath, result.DataPackagePath)
|
||||
}
|
||||
if renderer.runRequest.DataPackagePath != result.DataPackagePath {
|
||||
t.Fatalf("run DataPackagePath = %q, want managed path %q", renderer.runRequest.DataPackagePath, result.DataPackagePath)
|
||||
if renderer.structuredRunRequest.DataPackagePath != result.DataPackagePath {
|
||||
t.Fatalf("structured run DataPackagePath = %q, want managed path %q", renderer.structuredRunRequest.DataPackagePath, result.DataPackagePath)
|
||||
}
|
||||
if renderer.runRequest.OutputPath != result.ReportPath {
|
||||
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath)
|
||||
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
|
||||
t.Fatalf("structured run OutputPath = %q, want raw generated text path %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
|
||||
}
|
||||
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
|
||||
if got, want := strings.Join(store.calls, ","), "module_snapshot,data_package,preflight,metadata,generated_text_result,metadata,generated_text,metadata,render_context,metadata,prepare_report,metadata"; !strings.HasPrefix(got, want) {
|
||||
t.Fatalf("store calls = %v, want prefix %s", store.calls, want)
|
||||
}
|
||||
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
|
||||
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.GeneratedTextRawPath, result.GeneratedTextResultPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath, result.MetadataPath, outputPath)
|
||||
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read module snapshot: %v", err)
|
||||
@@ -252,7 +260,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read report output: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(report), "# Daily Report") {
|
||||
if !strings.Contains(string(report), "# Friday's Weather") {
|
||||
t.Fatalf("report output missing markdown:\n%s", string(report))
|
||||
}
|
||||
}
|
||||
@@ -754,8 +762,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
resolved, err := ResolveGenerate(GenerateRequest{
|
||||
Config: cfg,
|
||||
Report: ReportDaily,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Report: ReportThreeDay,
|
||||
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||
@@ -764,7 +771,7 @@ func TestGenerateReportSavesFinalMetadataForMarkdownAndGeneratedTextReports(t *t
|
||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||
Config: cfg,
|
||||
Resolved: resolved,
|
||||
Renderer: successfulRenderer("# Daily Report\n"),
|
||||
Renderer: successfulRenderer("# 3-Day Outlook\n"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateReport() error = %v", err)
|
||||
@@ -1125,7 +1132,7 @@ func TestGenerateReportNotifiesManagedReportPath(t *testing.T) {
|
||||
if req.PipelineID != "weatherreporter.daily" {
|
||||
t.Fatalf("notification PipelineID = %q, want rendered pipeline", req.PipelineID)
|
||||
}
|
||||
if req.BundleID != "weatherreporter.home.daily_today" {
|
||||
if req.BundleID != "weatherreporter.home.daily" {
|
||||
t.Fatalf("notification BundleID = %q, want default template", req.BundleID)
|
||||
}
|
||||
if req.IdempotencyKey != req.BundleID+"."+result.Metadata.RunID {
|
||||
@@ -1208,10 +1215,9 @@ func TestGenerateReportDoesNotNotifyAfterRenderOrRunFailure(t *testing.T) {
|
||||
{
|
||||
name: "Run",
|
||||
renderer: &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 2, Stderr: "run failed"},
|
||||
runErr: errors.New("run failed"),
|
||||
runBody: "# Daily Report\n",
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 2, Stderr: "run failed"},
|
||||
structuredRunErr: errors.New("run failed"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1350,12 +1356,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
|
||||
}
|
||||
renderer := &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{
|
||||
Stderr: "validation failed",
|
||||
ExitCode: 2,
|
||||
},
|
||||
runErr: errors.New("scriptorium run exited with code 2: validation failed"),
|
||||
runBody: "# Daily Report\n",
|
||||
structuredRunErr: errors.New("scriptorium run exited with code 2: validation failed"),
|
||||
}
|
||||
|
||||
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||
@@ -1366,8 +1371,8 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("GenerateReport() error = nil, want run error")
|
||||
}
|
||||
if renderer.renderCalls != 1 || renderer.runCalls != 1 {
|
||||
t.Fatalf("calls render=%d run=%d, want one of each", renderer.renderCalls, renderer.runCalls)
|
||||
if renderer.renderCalls != 1 || renderer.structuredRunCalls != 1 || renderer.runCalls != 0 {
|
||||
t.Fatalf("calls render=%d structured=%d run=%d, want render and structured run only", renderer.renderCalls, renderer.structuredRunCalls, renderer.runCalls)
|
||||
}
|
||||
store, err := state.NewFilesystemStore(cfg.Workspace)
|
||||
if err != nil {
|
||||
@@ -1380,8 +1385,11 @@ func TestGenerateReportReturnsRunErrorAfterPreflight(t *testing.T) {
|
||||
if _, err := os.Stat(paths.Metadata); err != nil {
|
||||
t.Fatalf("expected metadata for failed run %q: %v", paths.Metadata, err)
|
||||
}
|
||||
if _, err := os.Stat(paths.RenderedReport); err != nil {
|
||||
t.Fatalf("expected report from validation exit %q: %v", paths.RenderedReport, err)
|
||||
if _, err := os.Stat(paths.GeneratedTextRaw); err != nil {
|
||||
t.Fatalf("expected raw generated text from failed run %q: %v", paths.GeneratedTextRaw, err)
|
||||
}
|
||||
if _, err := os.Stat(paths.RenderedReport); !os.IsNotExist(err) {
|
||||
t.Fatalf("rendered report exists after failed generated-text run: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2552,6 +2560,10 @@ func validTodayGeneratedTextJSON() string {
|
||||
return `{"summary":"Today starts with showers before improving.","forecast_discussion":["Morning showers should taper as drier air arrives.","Afternoon conditions trend quieter."],"precipitation_timing":"The best rain chance is during the morning."}`
|
||||
}
|
||||
|
||||
func validDailyGeneratedTextJSON() string {
|
||||
return `{"summary":"Showers are possible during the selected day.","forecast_discussion":["A front will keep rain chances in the forecast.","Temperatures stay seasonable by afternoon."],"precipitation_timing":"Rain is most likely during the afternoon.","confidence":"Medium"}`
|
||||
}
|
||||
|
||||
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
|
||||
t.Helper()
|
||||
cfg.Workspace.Root = t.TempDir()
|
||||
@@ -2835,9 +2847,10 @@ func (s *recordingStore) SaveMetadata(ctx context.Context, metadata state.Metada
|
||||
|
||||
func successfulRenderer(body string) *recordingRenderer {
|
||||
return &recordingRenderer{
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
runBody: body,
|
||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||
runBody: body,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2913,6 +2926,9 @@ func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.Str
|
||||
if req.PromptID == "weather.tomorrow_generated_text" {
|
||||
body = validTomorrowGeneratedTextJSON()
|
||||
}
|
||||
if req.PromptID == "weather.daily_generated_text" {
|
||||
body = validDailyGeneratedTextJSON()
|
||||
}
|
||||
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2942,8 +2958,12 @@ func (r *recordingRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
|
||||
func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
||||
r.structuredRunCalls++
|
||||
r.structuredRunRequest = req
|
||||
if r.structuredRunBody != "" {
|
||||
if err := os.WriteFile(req.OutputPath, []byte(r.structuredRunBody), 0o600); err != nil {
|
||||
body := r.structuredRunBody
|
||||
if body == "" {
|
||||
body = validGeneratedTextJSONForPrompt(req.PromptID)
|
||||
}
|
||||
if body != "" {
|
||||
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -2952,3 +2972,18 @@ func (r *recordingRenderer) StructuredRun(_ context.Context, req scriptorium.Str
|
||||
}
|
||||
return r.structuredRunResult, r.structuredRunErr
|
||||
}
|
||||
|
||||
func validGeneratedTextJSONForPrompt(promptID string) string {
|
||||
switch promptID {
|
||||
case "weather.daily_generated_text":
|
||||
return validDailyGeneratedTextJSON()
|
||||
case "weather.today_generated_text":
|
||||
return validTodayGeneratedTextJSON()
|
||||
case "weather.tomorrow_generated_text":
|
||||
return validTomorrowGeneratedTextJSON()
|
||||
case "weather.hourly_generated_text":
|
||||
return validHourlyGeneratedTextJSON()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestMetadataModuleUsesPromptSafeSourceWarningSummary(t *testing.T) {
|
||||
t.Fatalf("BuildModule() error = %v", err)
|
||||
}
|
||||
value := moduleValue[MetadataModule](t, output)
|
||||
if value.RunID == "" || value.ReportID != report.DailyToday || value.PromptID != "weather.daily_report" {
|
||||
if value.RunID == "" || value.ReportID != report.Daily || value.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("metadata = %#v, want report identity", value)
|
||||
}
|
||||
if value.Location == nil || value.Location.Name != "Brentwood" {
|
||||
@@ -395,7 +395,7 @@ func TestAreaForecastDiscussionModuleUsesHourlyDefaultSections(t *testing.T) {
|
||||
|
||||
func testModuleContext() ModuleContext {
|
||||
generatedAt := mustParseModuleTime("2026-05-29T08:00:00-05:00")
|
||||
definition := report.DefaultRegistry().MustLookup(report.DailyToday)
|
||||
definition := report.DefaultRegistry().MustLookup(report.Daily)
|
||||
resolved := report.Resolved{
|
||||
Definition: definition,
|
||||
GeneratedAt: generatedAt,
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
|
||||
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary})
|
||||
if err != nil {
|
||||
@@ -67,7 +67,7 @@ func TestDerivedDailySummaryModulePackagesOrdinaryForecast(t *testing.T) {
|
||||
|
||||
func TestDerivedDailySummaryModuleFallsBackWithoutNarrativeFacts(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
ctx.Derived.DailySummaries[0].NarrativePeriods = nil
|
||||
|
||||
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDailySummary})
|
||||
@@ -89,7 +89,7 @@ func TestDerivedDailySummaryModuleFallsBackWithoutNarrativeFacts(t *testing.T) {
|
||||
|
||||
func TestPrecipTimingModuleHandlesRainyAndDryForecasts(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
|
||||
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.PrecipTiming})
|
||||
if err != nil {
|
||||
@@ -154,7 +154,7 @@ func TestPrecipTimingModuleUsesDerivedTimingWithoutDaypartSummaries(t *testing.T
|
||||
|
||||
func TestDerivedDaypartSummariesExposeConfiguredKeysAndHazards(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
|
||||
output, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DerivedDaypartSummaries})
|
||||
if err != nil {
|
||||
@@ -380,7 +380,7 @@ func TestDailyPlanningModulePackagesPlanningFields(t *testing.T) {
|
||||
|
||||
func TestDailyPlanningModuleRejectsUnsupportedReports(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
for _, id := range []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
|
||||
for _, id := range []report.ID{report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
|
||||
t.Run(string(id), func(t *testing.T) {
|
||||
ctx := derivedModuleContext(id)
|
||||
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.DailyPlanning})
|
||||
@@ -453,7 +453,7 @@ func TestTodayPlanningModulePackagesPlanningFields(t *testing.T) {
|
||||
|
||||
func TestTodayPlanningModuleRejectsUnsupportedReports(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
for _, id := range []report.ID{report.Tomorrow, report.DailyToday} {
|
||||
for _, id := range []report.ID{report.Tomorrow, report.Daily} {
|
||||
t.Run(string(id), func(t *testing.T) {
|
||||
ctx := derivedModuleContext(id)
|
||||
_, err := registry.BuildModule(ctx, module.ConfigItem{ID: module.TodayPlanning})
|
||||
@@ -484,7 +484,7 @@ func TestTodayPlanningModuleHandlesMissingDailySummary(t *testing.T) {
|
||||
|
||||
func TestDerivedModulesHandleMissingData(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
ctx.Derived.DailySummaries = nil
|
||||
ctx.Derived.DaypartSummaries = nil
|
||||
|
||||
@@ -576,7 +576,7 @@ func derivedModuleContext(id report.ID) ModuleContext {
|
||||
}
|
||||
|
||||
func todayModuleContext() ModuleContext {
|
||||
ctx := derivedModuleContext(report.DailyToday)
|
||||
ctx := derivedModuleContext(report.Daily)
|
||||
ctx.Resolved.Definition = report.Definition{
|
||||
ID: report.Today,
|
||||
Name: "Today Report",
|
||||
@@ -588,7 +588,7 @@ func todayModuleContext() ModuleContext {
|
||||
func dailyModuleContext() ModuleContext {
|
||||
ctx := derivedModuleContext(report.Tomorrow)
|
||||
ctx.Resolved.Definition = report.Definition{
|
||||
ID: report.ID("daily"),
|
||||
ID: report.Daily,
|
||||
Name: "Daily Report",
|
||||
PromptID: "weather.daily_generated_text",
|
||||
}
|
||||
|
||||
@@ -249,9 +249,8 @@ func (d ModuleDefinition) ValidateOptions(options any) error {
|
||||
}
|
||||
|
||||
func defaultModuleDefinitions() []ModuleDefinition {
|
||||
dailyReportID := report.ID("daily")
|
||||
allReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm}
|
||||
daypartReports := []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.ThreeDay, report.Weekend}
|
||||
allReports := []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm}
|
||||
daypartReports := []report.ID{report.Daily, report.Today, report.Tomorrow, report.ThreeDay, report.Weekend}
|
||||
return []ModuleDefinition{
|
||||
{
|
||||
ID: module.Metadata,
|
||||
@@ -277,7 +276,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
DefaultOptions: module.NarrativeForecastOptions{},
|
||||
RequiredCollected: []module.FactRequirement{module.CollectedNarrativeForecast},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedNarrativePeriods},
|
||||
SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow},
|
||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
||||
MissingData: module.MissingDataOmit,
|
||||
Builder: buildNarrativeForecastModule,
|
||||
},
|
||||
@@ -287,7 +286,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
DefaultOptions: module.HourlyForecastOptions{},
|
||||
RequiredCollected: []module.FactRequirement{module.CollectedHourlyForecast},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedHourlyPeriods},
|
||||
SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly},
|
||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow, report.Hourly},
|
||||
MissingData: module.MissingDataOmit,
|
||||
Builder: buildHourlyForecastModule,
|
||||
},
|
||||
@@ -296,7 +295,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
StanzaName: "derived_daily_summary",
|
||||
DefaultOptions: module.DerivedDailySummaryOptions{},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries, module.RequiresDerivedPrecipTiming},
|
||||
SupportedReports: []report.ID{report.DailyToday, report.Today, report.Tomorrow},
|
||||
SupportedReports: []report.ID{report.Daily, report.Today, report.Tomorrow},
|
||||
MissingData: module.MissingDataError,
|
||||
Builder: buildDerivedDailySummaryModule,
|
||||
},
|
||||
@@ -398,7 +397,7 @@ func defaultModuleDefinitions() []ModuleDefinition {
|
||||
StanzaName: "daily_planning",
|
||||
DefaultOptions: module.DailyPlanningOptions{},
|
||||
RequiredDerived: []module.FactRequirement{module.RequiresDerivedDailySummaries},
|
||||
SupportedReports: []report.ID{dailyReportID},
|
||||
SupportedReports: []report.ID{report.Daily},
|
||||
MissingData: module.MissingDataEmpty,
|
||||
Builder: buildDailyPlanningModule,
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestHourlyDefaultModuleOptions(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.ID("unknown")}})
|
||||
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.ID("unknown")}})
|
||||
if err == nil || !strings.Contains(err.Error(), `unknown module "unknown"`) {
|
||||
t.Fatalf("error = %v, want unknown module", err)
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestModuleRegistryRejectsUnknownModule(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsDuplicateModuleIDs(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
|
||||
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
|
||||
{ID: module.Metadata},
|
||||
{ID: module.Metadata},
|
||||
})
|
||||
@@ -106,8 +106,8 @@ func TestModuleRegistryRejectsDuplicateStanzaNames(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsIncompatibleReports(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{{ID: module.TomorrowPlanning}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily_today"`) {
|
||||
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.TomorrowPlanning}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "tomorrow_planning" is not compatible with report "daily"`) {
|
||||
t.Fatalf("error = %v, want incompatible report", err)
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestModuleRegistryValidatesTodayPlanningSupport(t *testing.T) {
|
||||
if err := registry.ValidateComposition(report.Today, []module.ConfigItem{{ID: module.TodayPlanning}}); err != nil {
|
||||
t.Fatalf("ValidateComposition(today) error = %v", err)
|
||||
}
|
||||
for _, id := range []report.ID{report.Tomorrow, report.DailyToday} {
|
||||
for _, id := range []report.ID{report.Tomorrow, report.Daily} {
|
||||
t.Run(string(id), func(t *testing.T) {
|
||||
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.TodayPlanning}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "today_planning" is not compatible with report`) {
|
||||
@@ -129,10 +129,10 @@ func TestModuleRegistryValidatesTodayPlanningSupport(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryValidatesDailyPlanningSupport(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
if err := registry.ValidateComposition(report.ID("daily"), []module.ConfigItem{{ID: module.DailyPlanning}}); err != nil {
|
||||
if err := registry.ValidateComposition(report.Daily, []module.ConfigItem{{ID: module.DailyPlanning}}); err != nil {
|
||||
t.Fatalf("ValidateComposition(daily) error = %v", err)
|
||||
}
|
||||
for _, id := range []report.ID{report.DailyToday, report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
|
||||
for _, id := range []report.ID{report.Today, report.Tomorrow, report.Hourly, report.ThreeDay, report.Weekend, report.Storm} {
|
||||
t.Run(string(id), func(t *testing.T) {
|
||||
err := registry.ValidateComposition(id, []module.ConfigItem{{ID: module.DailyPlanning}})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "daily_planning" is not compatible with report`) {
|
||||
@@ -204,7 +204,7 @@ func TestModuleRegistryRejectsUnsupportedMissingDataWarn(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
|
||||
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
|
||||
{ID: module.Metadata, Options: module.CurrentConditionsOptions{}},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), `module "metadata" options have type module.CurrentConditionsOptions, want module.MetadataOptions`) {
|
||||
@@ -214,7 +214,7 @@ func TestModuleRegistryRejectsInvalidOptionShapes(t *testing.T) {
|
||||
|
||||
func TestModuleRegistryAcceptsTypedOptions(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
err := registry.ValidateComposition(report.DailyToday, []module.ConfigItem{
|
||||
err := registry.ValidateComposition(report.Daily, []module.ConfigItem{
|
||||
{ID: module.Metadata, Options: module.MetadataOptions{}},
|
||||
{ID: module.CurrentConditions, Options: &module.CurrentConditionsOptions{}},
|
||||
})
|
||||
|
||||
@@ -185,7 +185,7 @@ func alertStatus(bundle *weatherdata.Bundle) *AlertStatus {
|
||||
|
||||
func variantForReport(id report.ID) string {
|
||||
switch id {
|
||||
case report.DailyToday, report.Today:
|
||||
case report.Daily, report.Today:
|
||||
return "today"
|
||||
case report.Tomorrow:
|
||||
return "tomorrow"
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestBatchOutputIncludesNotificationDetails(t *testing.T) {
|
||||
Failed: 1,
|
||||
Reports: []app.BatchReportResult{
|
||||
{
|
||||
ReportID: "daily_today",
|
||||
ReportID: "daily",
|
||||
Status: "succeeded",
|
||||
OutputPath: "/tmp/daily.md",
|
||||
NotificationStatus: "accepted",
|
||||
@@ -299,11 +299,11 @@ func TestBatchOutputDoesNotExposeSecretLikeNotificationErrors(t *testing.T) {
|
||||
Failed: 1,
|
||||
Reports: []app.BatchReportResult{
|
||||
{
|
||||
ReportID: "daily_today",
|
||||
ReportID: "daily",
|
||||
Status: "failed",
|
||||
Error: "notify report daily_today: upload failed: [redacted]",
|
||||
Error: "notify report daily: upload failed: [redacted]",
|
||||
NotificationStatus: "failed",
|
||||
NotificationError: "notify report daily_today: upload failed: [redacted]",
|
||||
NotificationError: "notify report daily: upload failed: [redacted]",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -526,7 +526,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read report: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(report), "# Daily Report") {
|
||||
if !strings.Contains(string(report), "# Friday's Weather") {
|
||||
t.Fatalf("report output missing markdown:\n%s", string(report))
|
||||
}
|
||||
dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "daily", "2026-05-29", "*.data_package.yaml"))
|
||||
@@ -540,7 +540,7 @@ func TestRunGenerateDailyWritesMarkdownReport(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read managed data package: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || !strings.Contains(string(data), "id: daily_today") {
|
||||
if !strings.Contains(string(data), "schema_version: weatherreporter.data_package.v2") || !strings.Contains(string(data), "id: daily") {
|
||||
t.Fatalf("data package output missing expected content:\n%s", string(data))
|
||||
}
|
||||
if !strings.Contains(string(data), "location:") ||
|
||||
@@ -1299,6 +1299,21 @@ JSON
|
||||
],
|
||||
"precipitation_timing": "The best rain chance is during the morning."
|
||||
}
|
||||
JSON
|
||||
printf 'wrote generated text\n' >&2
|
||||
exit 0
|
||||
fi
|
||||
if [ "$prompt" = "weather.daily_generated_text" ]; then
|
||||
cat > "$out" <<'JSON'
|
||||
{
|
||||
"summary": "Showers are possible during the selected day.",
|
||||
"forecast_discussion": [
|
||||
"A front will keep rain chances in the forecast.",
|
||||
"Temperatures stay seasonable by afternoon."
|
||||
],
|
||||
"precipitation_timing": "Rain is most likely during the afternoon.",
|
||||
"confidence": "Medium"
|
||||
}
|
||||
JSON
|
||||
printf 'wrote generated text\n' >&2
|
||||
exit 0
|
||||
@@ -1345,6 +1360,21 @@ if [ "$1" = "run" ]; then
|
||||
"precipitation_timing": "A cold front is moving into the region.",
|
||||
"confidence": "Medium"
|
||||
}
|
||||
JSON
|
||||
printf 'wrote generated text\n' >&2
|
||||
exit 0
|
||||
fi
|
||||
if [ "$prompt" = "weather.daily_generated_text" ]; then
|
||||
cat > "$out" <<'JSON'
|
||||
{
|
||||
"summary": "Showers are possible during the selected day.",
|
||||
"forecast_discussion": [
|
||||
"A front will keep rain chances in the forecast.",
|
||||
"Temperatures stay seasonable by afternoon."
|
||||
],
|
||||
"precipitation_timing": "Rain is most likely during the afternoon.",
|
||||
"confidence": "Medium"
|
||||
}
|
||||
JSON
|
||||
printf 'wrote generated text\n' >&2
|
||||
exit 0
|
||||
|
||||
@@ -163,7 +163,7 @@ reports:
|
||||
if err != nil {
|
||||
t.Fatalf("ReportModuleOverrides() error = %v", err)
|
||||
}
|
||||
items := overrides[report.DailyToday]
|
||||
items := overrides[report.Daily]
|
||||
if len(items) != 6 {
|
||||
t.Fatalf("daily override length = %d, want 6", len(items))
|
||||
}
|
||||
@@ -220,8 +220,8 @@ reports:
|
||||
t.Fatalf("today override[%d] = %s, want %s", i, items[i].ID, id)
|
||||
}
|
||||
}
|
||||
if _, ok := overrides[report.DailyToday]; ok {
|
||||
t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.DailyToday])
|
||||
if _, ok := overrides[report.Daily]; ok {
|
||||
t.Fatalf("daily override = %#v, want today override to stay distinct", overrides[report.Daily])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,12 +320,12 @@ func TestValidateReportModuleAliasesDirectly(t *testing.T) {
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "DuplicateAlias",
|
||||
name: "RetiredDailyReportID",
|
||||
reports: map[string]ReportConfig{
|
||||
"daily": {},
|
||||
"daily_today": {},
|
||||
},
|
||||
wantErr: "duplicates report override",
|
||||
wantErr: "reports.daily_today",
|
||||
},
|
||||
{
|
||||
name: "TodayAndDailyAreDistinct",
|
||||
@@ -425,7 +425,7 @@ reports:
|
||||
deterministic_modules:
|
||||
- tomorrow_planning
|
||||
`,
|
||||
wantErr: `not compatible with report "daily_today"`,
|
||||
wantErr: `not compatible with report "daily"`,
|
||||
},
|
||||
{
|
||||
name: "RemovedPlaceholderModule",
|
||||
@@ -461,7 +461,7 @@ reports:
|
||||
wantErr: "options are invalid",
|
||||
},
|
||||
{
|
||||
name: "DuplicateReportAlias",
|
||||
name: "RetiredDailyReportID",
|
||||
yaml: `
|
||||
reports:
|
||||
daily:
|
||||
@@ -471,7 +471,7 @@ reports:
|
||||
deterministic_modules:
|
||||
- current_conditions
|
||||
`,
|
||||
wantErr: "duplicates report override",
|
||||
wantErr: "reports.daily_today",
|
||||
},
|
||||
{
|
||||
name: "HourlyIncompatibleDailyModule",
|
||||
|
||||
@@ -115,7 +115,7 @@ func BuildDerived(req BuildDerivedRequest) (DerivedFacts, error) {
|
||||
|
||||
switch req.Resolved.Definition.ID {
|
||||
case report.Hourly:
|
||||
case report.DailyToday, report.Today, report.Tomorrow:
|
||||
case report.Daily, report.Today, report.Tomorrow:
|
||||
summary, err := forecast.BuildDailySummary(bundle, period.Start, location, req.Dayparts)
|
||||
if err != nil {
|
||||
return DerivedFacts{}, err
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestBuildCollectedCopiesBundleFactsAndKeepsSourcesSeparate(t *testing.T) {
|
||||
|
||||
func TestBuildDerivedDailySlicesDaypartsAndAlerts(t *testing.T) {
|
||||
location := testLocation()
|
||||
resolved := resolveForTest(t, report.DailyToday, mustParse("2026-05-29T08:00:00-05:00"), location)
|
||||
resolved := resolveForTest(t, report.Daily, mustParse("2026-05-29T08:00:00-05:00"), location)
|
||||
derived, err := BuildDerived(BuildDerivedRequest{
|
||||
Resolved: resolved,
|
||||
Timezone: location.String(),
|
||||
@@ -253,7 +253,7 @@ func TestBuildDerivedSelectsSPCConvectiveOutlooksByValidPeriod(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "daily today",
|
||||
resolved: resolveForTest(t, report.DailyToday, now, location),
|
||||
resolved: resolveForTest(t, report.Daily, now, location),
|
||||
wantOutlookIDs: []string{"fri-high", "fri-storm", "fri-low", "fri-missing-rank", "fri-probabilistic"},
|
||||
wantDiscussion: []string{"day1 early", "day1 late"},
|
||||
},
|
||||
@@ -322,7 +322,7 @@ func TestBuildDerivedUnsupportedReportReturnsActionableError(t *testing.T) {
|
||||
|
||||
func TestBuildDerivedSPCConvectiveOutlooksDistinguishesMissingAndCheckedEmpty(t *testing.T) {
|
||||
location := testLocation()
|
||||
resolved := resolveForTest(t, report.DailyToday, mustParse("2026-05-29T08:00:00-05:00"), location)
|
||||
resolved := resolveForTest(t, report.Daily, mustParse("2026-05-29T08:00:00-05:00"), location)
|
||||
bundle := testBundle(location)
|
||||
bundle.SPCConvectiveOutlooks = nil
|
||||
|
||||
@@ -465,7 +465,11 @@ func testDayparts() []forecast.DaypartDefinition {
|
||||
|
||||
func resolveForTest(t *testing.T, id report.ID, now time.Time, location *time.Location) report.Resolved {
|
||||
t.Helper()
|
||||
resolved, err := report.Resolve(id, report.ResolveRequest{Now: now, Location: location})
|
||||
req := report.ResolveRequest{Now: now, Location: location}
|
||||
if id == report.Daily {
|
||||
req.Date = now
|
||||
}
|
||||
resolved, err := report.Resolve(id, req)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve %s: %v", id, err)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestCatalogLookupSupportsTodayDefinition(t *testing.T) {
|
||||
|
||||
func TestCatalogLookupSupportsDailyDefinitionAssets(t *testing.T) {
|
||||
definition := report.Definition{
|
||||
ID: report.ID("daily"),
|
||||
ID: report.Daily,
|
||||
GenerationMode: report.GenerationModeGeneratedTextTemplate,
|
||||
GeneratedTextSchemaID: "daily",
|
||||
TemplateID: "daily",
|
||||
@@ -206,7 +206,7 @@ func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) {
|
||||
}
|
||||
|
||||
dailyHandler, err := LookupDefinition(report.Definition{
|
||||
ID: report.ID("daily"),
|
||||
ID: report.Daily,
|
||||
GenerationMode: report.GenerationModeGeneratedTextTemplate,
|
||||
GeneratedTextSchemaID: "daily",
|
||||
TemplateID: "daily",
|
||||
@@ -281,7 +281,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
}
|
||||
|
||||
dailyHandler, err := LookupDefinition(report.Definition{
|
||||
ID: report.ID("daily"),
|
||||
ID: report.Daily,
|
||||
GenerationMode: report.GenerationModeGeneratedTextTemplate,
|
||||
GeneratedTextSchemaID: "daily",
|
||||
TemplateID: "daily",
|
||||
@@ -303,7 +303,7 @@ func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
|
||||
|
||||
func TestCatalogBuildRenderContextSupportsDaily(t *testing.T) {
|
||||
handler, err := LookupDefinition(report.Definition{
|
||||
ID: report.ID("daily"),
|
||||
ID: report.Daily,
|
||||
GenerationMode: report.GenerationModeGeneratedTextTemplate,
|
||||
GeneratedTextSchemaID: "daily",
|
||||
TemplateID: "daily",
|
||||
|
||||
@@ -290,7 +290,7 @@ func TestBuildDailyRenderContext(t *testing.T) {
|
||||
if !ctx.Report.ValidPeriod.Start.Equal(metadata.ValidPeriod.Start) || !ctx.Report.ValidPeriod.End.Equal(metadata.ValidPeriod.End) || ctx.Report.Timezone != "America/Chicago" {
|
||||
t.Fatalf("period/timezone = %#v/%q, want metadata passthrough", ctx.Report.ValidPeriod, ctx.Report.Timezone)
|
||||
}
|
||||
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.ID("daily") {
|
||||
if ctx.Modules.Metadata == nil || ctx.Modules.Metadata.ReportID != report.Daily {
|
||||
t.Fatalf("Modules.Metadata = %#v, want daily metadata", ctx.Modules.Metadata)
|
||||
}
|
||||
if ctx.Modules.CurrentConditions == nil || ctx.Modules.HourlyForecast == nil {
|
||||
@@ -544,7 +544,7 @@ func testDailyMetadata() briefing.Metadata {
|
||||
generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC)
|
||||
return briefing.Metadata{
|
||||
RunID: "run-daily",
|
||||
ReportID: report.ID("daily"),
|
||||
ReportID: report.Daily,
|
||||
PromptID: "weather.daily_generated_text",
|
||||
GeneratedAt: generatedAt,
|
||||
Units: "imperial",
|
||||
@@ -763,7 +763,7 @@ func testDailySnapshot(t *testing.T) module.Snapshot {
|
||||
StanzaName: string(module.Metadata),
|
||||
Value: briefing.MetadataModule{
|
||||
RunID: "run-daily",
|
||||
ReportID: report.ID("daily"),
|
||||
ReportID: report.Daily,
|
||||
PromptID: "weather.daily_generated_text",
|
||||
GeneratedAt: testDailyMetadata().GeneratedAt,
|
||||
Units: "imperial",
|
||||
|
||||
@@ -40,6 +40,7 @@ var briefingStanzaCategories = map[string]string{
|
||||
string(module.DerivedDaypartSummaries): categoryDerivedSummaries,
|
||||
string(module.PrecipTiming): categoryDerivedSummaries,
|
||||
string(module.OutdoorWindows): categoryDerivedSummaries,
|
||||
string(module.DailyPlanning): categoryDerivedSummaries,
|
||||
string(module.TodayPlanning): categoryDerivedSummaries,
|
||||
string(module.TomorrowPlanning): categoryDerivedSummaries,
|
||||
string(module.NarrativeForecast): categoryNarrativeProducts,
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestBuildDailyDataPackage(t *testing.T) {
|
||||
if pkg.SchemaVersion != SchemaVersion {
|
||||
t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion)
|
||||
}
|
||||
if pkg.RunID != "20260529T100000Z_daily_today" {
|
||||
if pkg.RunID != "20260529T100000Z_daily" {
|
||||
t.Fatalf("RunID = %q, want metadata run id", pkg.RunID)
|
||||
}
|
||||
if pkg.Report.PromptID != "weather.daily_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_report", pkg.Report.PromptID)
|
||||
if pkg.Report.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_generated_text", pkg.Report.PromptID)
|
||||
}
|
||||
if pkg.Report.CurrentLocalDate != "2026-05-29" {
|
||||
t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate)
|
||||
@@ -252,10 +252,10 @@ func TestMarshalYAMLRejectsUncategorizedStanza(t *testing.T) {
|
||||
func TestLoadYAMLRejectsMisplacedStanza(t *testing.T) {
|
||||
data := []byte(`
|
||||
schema_version: weatherreporter.data_package.v2
|
||||
run_id: 20260529T100000Z_daily_today
|
||||
run_id: 20260529T100000Z_daily
|
||||
report:
|
||||
id: daily_today
|
||||
prompt_id: weather.daily_report
|
||||
id: daily
|
||||
prompt_id: weather.daily_generated_text
|
||||
generated_at: 2026-05-29T10:00:00Z
|
||||
timezone: America/Chicago
|
||||
current_local_date: "2026-05-29"
|
||||
@@ -264,7 +264,7 @@ report:
|
||||
end: 2026-05-30T05:00:00Z
|
||||
briefing:
|
||||
metadata:
|
||||
run_id: 20260529T100000Z_daily_today
|
||||
run_id: 20260529T100000Z_daily
|
||||
raw_data:
|
||||
alert_digest:
|
||||
checked: true
|
||||
@@ -283,10 +283,10 @@ func validBuildRequest(t *testing.T) BuildRequest {
|
||||
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
|
||||
return BuildRequest{
|
||||
Metadata: Metadata{
|
||||
RunID: "20260529T100000Z_daily_today",
|
||||
ReportID: report.DailyToday,
|
||||
RunID: "20260529T100000Z_daily",
|
||||
ReportID: report.Daily,
|
||||
Variant: "today",
|
||||
PromptID: "weather.daily_report",
|
||||
PromptID: "weather.daily_generated_text",
|
||||
GeneratedAt: generatedAt,
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: timeutil.Period{
|
||||
@@ -295,7 +295,7 @@ func validBuildRequest(t *testing.T) BuildRequest {
|
||||
},
|
||||
},
|
||||
Modules: snapshotWithOutputs(t,
|
||||
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily_today"}},
|
||||
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily"}},
|
||||
module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]string{"condition_text": "Partly cloudy"}},
|
||||
module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]string{"date": "2026-05-29"}},
|
||||
module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: map[string]bool{"checked": true}},
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
func dailyTodayDefinition() Definition {
|
||||
func dailyDefinition() Definition {
|
||||
return Definition{
|
||||
ID: DailyToday,
|
||||
Name: "Daily Report",
|
||||
PromptID: "weather.daily_report",
|
||||
GenerationMode: GenerationModeScriptoriumMarkdown,
|
||||
ComparisonStrategy: CompareSameValidDate,
|
||||
ArtifactGroup: "daily",
|
||||
BatchOutputName: "daily.md",
|
||||
Generated: true,
|
||||
CompatiblePriorIDs: []ID{DailyToday},
|
||||
Modules: dailyTodayModules(),
|
||||
Morning: true,
|
||||
resolve: resolveDailyToday,
|
||||
ID: Daily,
|
||||
Name: "Daily Report",
|
||||
PromptID: "weather.daily_generated_text",
|
||||
GenerationMode: GenerationModeGeneratedTextTemplate,
|
||||
TemplateID: "daily",
|
||||
GeneratedTextSchemaID: "daily",
|
||||
ComparisonStrategy: CompareSameValidDate,
|
||||
ArtifactGroup: "daily",
|
||||
BatchOutputName: "daily.md",
|
||||
Generated: true,
|
||||
CompatiblePriorIDs: []ID{Daily},
|
||||
Modules: dailyModules(),
|
||||
resolve: resolveDaily,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +44,7 @@ func tomorrowDefinition() Definition {
|
||||
}
|
||||
}
|
||||
|
||||
func dailyTodayModules() []module.ConfigItem {
|
||||
func dailyModules() []module.ConfigItem {
|
||||
return moduleItems(
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
@@ -55,6 +58,7 @@ func dailyTodayModules() []module.ConfigItem {
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.DailyPlanning,
|
||||
module.HourlyForecast,
|
||||
)
|
||||
}
|
||||
@@ -78,11 +82,11 @@ func tomorrowModules() []module.ConfigItem {
|
||||
)
|
||||
}
|
||||
|
||||
func resolveDailyToday(req ResolveRequest) (timeutil.Period, error) {
|
||||
if !req.Date.IsZero() {
|
||||
return timeutil.CivilDay(req.Date, req.Location), nil
|
||||
func resolveDaily(req ResolveRequest) (timeutil.Period, error) {
|
||||
if req.Date.IsZero() {
|
||||
return timeutil.Period{}, fmt.Errorf("daily report requires an explicit date")
|
||||
}
|
||||
return timeutil.CivilDay(req.Now, req.Location), nil
|
||||
return timeutil.CivilDay(req.Date, req.Location), nil
|
||||
}
|
||||
|
||||
func resolveTomorrow(req ResolveRequest) (timeutil.Period, error) {
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
type ID string
|
||||
|
||||
const (
|
||||
DailyToday ID = "daily_today"
|
||||
Today ID = "today"
|
||||
Tomorrow ID = "tomorrow"
|
||||
Hourly ID = "hourly"
|
||||
ThreeDay ID = "three_day"
|
||||
Weekend ID = "weekend"
|
||||
Storm ID = "storm"
|
||||
Daily ID = "daily"
|
||||
Today ID = "today"
|
||||
Tomorrow ID = "tomorrow"
|
||||
Hourly ID = "hourly"
|
||||
ThreeDay ID = "three_day"
|
||||
Weekend ID = "weekend"
|
||||
Storm ID = "storm"
|
||||
)
|
||||
|
||||
type ComparisonStrategy string
|
||||
|
||||
@@ -21,7 +21,7 @@ const (
|
||||
func IDForCommandName(name string) (ID, error) {
|
||||
switch name {
|
||||
case CommandNameDaily:
|
||||
return DailyToday, nil
|
||||
return Daily, nil
|
||||
case CommandNameToday:
|
||||
return Today, nil
|
||||
case CommandNameTomorrow:
|
||||
@@ -54,8 +54,8 @@ func CommandNames() []string {
|
||||
func IDForConfigKey(key string) (ID, error) {
|
||||
normalized := strings.ReplaceAll(strings.TrimSpace(strings.ToLower(key)), "-", "_")
|
||||
switch normalized {
|
||||
case "daily", "daily_today":
|
||||
return DailyToday, nil
|
||||
case "daily":
|
||||
return Daily, nil
|
||||
case "today":
|
||||
return Today, nil
|
||||
case "tomorrow":
|
||||
|
||||
@@ -14,11 +14,14 @@ func TestDailyValidPeriod(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
|
||||
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location})
|
||||
if err == nil {
|
||||
t.Fatal("Resolve() error = nil, want explicit date requirement")
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00")
|
||||
if !strings.Contains(err.Error(), "requires an explicit date") {
|
||||
t.Fatalf("Resolve() error = %v, want explicit date requirement", err)
|
||||
}
|
||||
_ = resolved
|
||||
}
|
||||
|
||||
func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
|
||||
@@ -26,11 +29,23 @@ func TestDailyValidPeriodCanUseExplicitDate(t *testing.T) {
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
date := mustParse("2026-05-31T12:00:00-05:00")
|
||||
|
||||
resolved, err := Resolve(DailyToday, ResolveRequest{Now: now, Location: location, Date: date})
|
||||
resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location, Date: date})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00")
|
||||
if resolved.Definition.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_generated_text", resolved.Definition.PromptID)
|
||||
}
|
||||
if resolved.Definition.GenerationMode != GenerationModeGeneratedTextTemplate {
|
||||
t.Fatalf("GenerationMode = %q, want generated_text_template", resolved.Definition.GenerationMode)
|
||||
}
|
||||
if resolved.Definition.TemplateID != "daily" {
|
||||
t.Fatalf("TemplateID = %q, want daily", resolved.Definition.TemplateID)
|
||||
}
|
||||
if resolved.Definition.GeneratedTextSchemaID != "daily" {
|
||||
t.Fatalf("GeneratedTextSchemaID = %q, want daily", resolved.Definition.GeneratedTextSchemaID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodayValidPeriod(t *testing.T) {
|
||||
@@ -256,7 +271,7 @@ func TestEveningBatchIncludesTomorrow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchesDoNotIncludeHourly(t *testing.T) {
|
||||
func TestBatchesDoNotIncludeManualReports(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
req := ResolveRequest{Now: mustParse("2026-05-29T06:00:00-05:00"), Location: location}
|
||||
|
||||
@@ -269,8 +284,8 @@ func TestBatchesDoNotIncludeHourly(t *testing.T) {
|
||||
t.Fatalf("BatchReports(evening) error = %v", err)
|
||||
}
|
||||
for _, resolved := range append(morning, evening...) {
|
||||
if resolved.Definition.ID == Hourly {
|
||||
t.Fatalf("batch included %q, want hourly excluded", resolved.Definition.ID)
|
||||
if resolved.Definition.ID == Hourly || resolved.Definition.ID == Daily {
|
||||
t.Fatalf("batch included %q, want manual reports excluded", resolved.Definition.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +295,7 @@ func TestIDForCommandName(t *testing.T) {
|
||||
name string
|
||||
want ID
|
||||
}{
|
||||
{name: "daily", want: DailyToday},
|
||||
{name: "daily", want: Daily},
|
||||
{name: "today", want: Today},
|
||||
{name: "tomorrow", want: Tomorrow},
|
||||
{name: "hourly", want: Hourly},
|
||||
@@ -323,8 +338,7 @@ func TestIDForConfigKey(t *testing.T) {
|
||||
key string
|
||||
want ID
|
||||
}{
|
||||
{key: "daily", want: DailyToday},
|
||||
{key: "daily_today", want: DailyToday},
|
||||
{key: "daily", want: Daily},
|
||||
{key: "today", want: Today},
|
||||
{key: "tomorrow", want: Tomorrow},
|
||||
{key: "hourly", want: Hourly},
|
||||
@@ -351,6 +365,9 @@ func TestIDForConfigKey(t *testing.T) {
|
||||
if _, err := IDForConfigKey("daily_tomorrow"); err == nil || !strings.Contains(err.Error(), `report config key "daily_tomorrow" is not a known report`) {
|
||||
t.Fatalf("IDForConfigKey(daily_tomorrow) error = %v, want unknown key", err)
|
||||
}
|
||||
if _, err := IDForConfigKey("daily_today"); err == nil || !strings.Contains(err.Error(), `report config key "daily_today" is not a known report`) {
|
||||
t.Fatalf("IDForConfigKey(daily_today) error = %v, want unknown key", err)
|
||||
}
|
||||
dailyID, err := IDForConfigKey("daily")
|
||||
if err != nil {
|
||||
t.Fatalf("IDForConfigKey(daily) error = %v", err)
|
||||
@@ -418,7 +435,7 @@ func TestRegistryLookupErrorIsActionable(t *testing.T) {
|
||||
|
||||
func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) {
|
||||
ids := resolvedDefinitionIDs(DefaultRegistry().All())
|
||||
want := []string{"daily_today", "today", "tomorrow", "hourly", "three_day", "weekend", "storm"}
|
||||
want := []string{"daily", "today", "tomorrow", "hourly", "three_day", "weekend", "storm"}
|
||||
if strings.Join(ids, ",") != strings.Join(want, ",") {
|
||||
t.Fatalf("All() ids = %#v, want %#v", ids, want)
|
||||
}
|
||||
@@ -443,7 +460,7 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
|
||||
if !definition.Generated {
|
||||
continue
|
||||
}
|
||||
if definition.ID == Hourly || definition.ID == Today || definition.ID == Tomorrow {
|
||||
if definition.ID == Hourly || definition.ID == Daily || definition.ID == Today || definition.ID == Tomorrow {
|
||||
wantTemplate := string(definition.ID)
|
||||
if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
|
||||
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
|
||||
@@ -478,11 +495,11 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
comparisonStrategy ComparisonStrategy
|
||||
}{
|
||||
{
|
||||
id: DailyToday,
|
||||
id: Daily,
|
||||
artifactGroup: "daily",
|
||||
batchOutputName: "daily.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{DailyToday},
|
||||
compatiblePriorIDs: []ID{Daily},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
@@ -572,7 +589,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
want []module.ID
|
||||
}{
|
||||
{
|
||||
id: DailyToday,
|
||||
id: Daily,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
@@ -586,6 +603,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.DailyPlanning,
|
||||
module.HourlyForecast,
|
||||
},
|
||||
},
|
||||
@@ -703,7 +721,7 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
|
||||
base := DefaultRegistry()
|
||||
overridden, err := base.WithModuleOverrides(map[ID][]module.ConfigItem{
|
||||
DailyToday: {
|
||||
Daily: {
|
||||
{ID: module.Metadata},
|
||||
{ID: module.AlertDigest},
|
||||
},
|
||||
@@ -712,7 +730,7 @@ func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
|
||||
t.Fatalf("WithModuleOverrides() error = %v", err)
|
||||
}
|
||||
|
||||
definition, err := overridden.Lookup(DailyToday)
|
||||
definition, err := overridden.Lookup(Daily)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(overridden) error = %v", err)
|
||||
}
|
||||
@@ -720,7 +738,7 @@ func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
|
||||
t.Fatalf("overridden ModuleIDs() = %#v, want metadata and alert digest", definition.ModuleIDs())
|
||||
}
|
||||
|
||||
defaultDefinition, err := base.Lookup(DailyToday)
|
||||
defaultDefinition, err := base.Lookup(Daily)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(default) error = %v", err)
|
||||
}
|
||||
@@ -743,18 +761,22 @@ func TestRegistryRejectsModuleOverrideForUnknownReport(t *testing.T) {
|
||||
|
||||
func TestResolvedMetadata(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
resolved, err := Resolve(DailyToday, ResolveRequest{Now: mustParse("2026-05-29T05:00:00-05:00"), Location: location})
|
||||
resolved, err := Resolve(Daily, ResolveRequest{
|
||||
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||
Location: location,
|
||||
Date: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
metadata := resolved.Metadata()
|
||||
if metadata.ReportID != DailyToday {
|
||||
t.Fatalf("ReportID = %q, want daily_today", metadata.ReportID)
|
||||
if metadata.ReportID != Daily {
|
||||
t.Fatalf("ReportID = %q, want daily", metadata.ReportID)
|
||||
}
|
||||
if metadata.PromptID != "weather.daily_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_report", metadata.PromptID)
|
||||
if metadata.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_generated_text", metadata.PromptID)
|
||||
}
|
||||
if !strings.Contains(metadata.RunID, "daily_today") {
|
||||
if !strings.Contains(metadata.RunID, "daily") {
|
||||
t.Fatalf("RunID = %q, want report id", metadata.RunID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Registry struct {
|
||||
|
||||
func DefaultRegistry() Registry {
|
||||
definitions := []Definition{
|
||||
dailyTodayDefinition(),
|
||||
dailyDefinition(),
|
||||
todayDefinition(),
|
||||
tomorrowDefinition(),
|
||||
hourlyDefinition(),
|
||||
@@ -75,7 +75,7 @@ func (r Registry) MustLookup(id ID) Definition {
|
||||
}
|
||||
|
||||
func (r Registry) All() []Definition {
|
||||
ids := []ID{DailyToday, Today, Tomorrow, Hourly, ThreeDay, Weekend, Storm}
|
||||
ids := []ID{Daily, Today, Tomorrow, Hourly, ThreeDay, Weekend, Storm}
|
||||
out := make([]Definition, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if definition, ok := r.definitions[id]; ok {
|
||||
|
||||
@@ -27,12 +27,12 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.modules.json"),
|
||||
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"),
|
||||
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.yaml"),
|
||||
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"),
|
||||
filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.distributor.json"),
|
||||
filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily_today.md"),
|
||||
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.modules.json"),
|
||||
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.metadata.json"),
|
||||
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.data_package.yaml"),
|
||||
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.render.json"),
|
||||
filepath.Join("notifications", "daily", "2026-05-29", "20260529T100000.000000000Z_daily.distributor.json"),
|
||||
filepath.Join("reports", "daily", "20260529T100000.000000000Z_daily.md"),
|
||||
} {
|
||||
if !strings.Contains(pathsString(paths), want) {
|
||||
t.Fatalf("paths = %#v, want component %q", paths, want)
|
||||
@@ -49,6 +49,13 @@ func TestGeneratedTextArtifactPathsUseSnapshotTree(t *testing.T) {
|
||||
validDate string
|
||||
runID string
|
||||
}{
|
||||
{
|
||||
name: "daily",
|
||||
resolved: resolveDailyAt(t, "2026-05-29T05:00:00-05:00"),
|
||||
group: "daily",
|
||||
validDate: "2026-05-29",
|
||||
runID: "20260529T100000.000000000Z_daily",
|
||||
},
|
||||
{
|
||||
name: "hourly",
|
||||
resolved: resolveHourlyAt(t, "2026-05-29T05:00:00-05:00"),
|
||||
@@ -243,9 +250,12 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
|
||||
if strings.Contains(string(data), "MetadataPath") || strings.Contains(string(data), "metadataPath") {
|
||||
t.Fatalf("metadata JSON includes runtime-only MetadataPath:\n%s", string(data))
|
||||
}
|
||||
for _, unexpected := range []string{"generatedTextSchemaId", "generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} {
|
||||
if decoded.GeneratedTextSchemaID != "daily" {
|
||||
t.Fatalf("GeneratedTextSchemaID = %q, want daily", decoded.GeneratedTextSchemaID)
|
||||
}
|
||||
for _, unexpected := range []string{"generatedTextRawPath", "generatedTextResultPath", "generatedTextPath", "renderContextPath"} {
|
||||
if strings.Contains(string(data), unexpected) {
|
||||
t.Fatalf("metadata JSON includes generated-text field %q for Markdown report:\n%s", unexpected, string(data))
|
||||
t.Fatalf("metadata JSON includes unsaved generated-text path field %q:\n%s", unexpected, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,9 +557,10 @@ func resolveDailyAt(t *testing.T, value string) report.Resolved {
|
||||
if err != nil {
|
||||
t.Fatalf("parse time: %v", err)
|
||||
}
|
||||
resolved, err := report.DefaultRegistry().Resolve(report.DailyToday, report.ResolveRequest{
|
||||
resolved, err := report.DefaultRegistry().Resolve(report.Daily, report.ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
Date: now,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
|
||||
Reference in New Issue
Block a user