Add manual Storm Report generation
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
reports from normalized forecast data.
|
reports from normalized forecast data.
|
||||||
|
|
||||||
The application can currently generate Daily Today, Daily Tomorrow, 3-Day
|
The application can currently generate Daily Today, Daily Tomorrow, 3-Day
|
||||||
Outlook, and Weekend Outlook Markdown reports through `scriptorium`, with
|
Outlook, Weekend Outlook, and manual Storm Report Markdown reports through `scriptorium`, with
|
||||||
inspectable briefing, prompt input, preflight, report, and metadata artifacts
|
inspectable briefing, prompt input, preflight, report, and metadata artifacts
|
||||||
under the configured workspace.
|
under the configured workspace.
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ weatherreporter generate daily --date 2026-05-29 --out ./daily.md
|
|||||||
weatherreporter generate tomorrow --out ./tomorrow.md
|
weatherreporter generate tomorrow --out ./tomorrow.md
|
||||||
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
|
||||||
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
32
docs/cli.md
32
docs/cli.md
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
`weatherreporter generate daily`, `weatherreporter generate tomorrow`,
|
`weatherreporter generate daily`, `weatherreporter generate tomorrow`,
|
||||||
`weatherreporter generate three-day`, `weatherreporter generate weekend`,
|
`weatherreporter generate three-day`, `weatherreporter generate weekend`,
|
||||||
|
`weatherreporter generate storm`,
|
||||||
`weatherreporter run morning`, and `weatherreporter run evening` currently
|
`weatherreporter run morning`, and `weatherreporter run evening` currently
|
||||||
write Markdown reports through `scriptorium`, after writing managed preparation
|
write Markdown reports through `scriptorium`, after writing managed preparation
|
||||||
artifacts and running `scriptorium render` as a preflight check. Unsupported
|
artifacts and running `scriptorium render` as a preflight check.
|
||||||
report generation still resolves configuration, report definitions, and valid
|
|
||||||
periods, then returns a not-implemented error.
|
|
||||||
|
|
||||||
## Shortest Useful Command
|
## Shortest Useful Command
|
||||||
|
|
||||||
@@ -35,6 +34,12 @@ weatherreporter generate three-day --out ./three-day.md
|
|||||||
weatherreporter generate weekend --out ./weekend.md
|
weatherreporter generate weekend --out ./weekend.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a focused manual Storm Report:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
||||||
|
```
|
||||||
|
|
||||||
## Command Overview
|
## Command Overview
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -47,15 +52,16 @@ weatherreporter run morning
|
|||||||
weatherreporter run evening
|
weatherreporter run evening
|
||||||
```
|
```
|
||||||
|
|
||||||
`generate daily`, `generate tomorrow`, `generate three-day`, and
|
`generate daily`, `generate tomorrow`, `generate three-day`,
|
||||||
`generate weekend` write a briefing snapshot, prompt input data package, render
|
`generate weekend`, and `generate storm` write a briefing snapshot, prompt
|
||||||
preflight output, Markdown report, and metadata file under the configured
|
input data package, render preflight output, Markdown report, and metadata file
|
||||||
workspace. `run evening` generates the Tomorrow Planning Brief. `run morning`
|
under the configured workspace. `generate storm` requires explicit `--start`
|
||||||
generates Daily Today and the 3-Day Outlook, plus Weekend Outlook except on
|
and `--end` bounds for the event window. `run evening` generates the Tomorrow
|
||||||
Sunday. Run commands continue remaining reports after an independent report
|
Planning Brief. `run morning` generates Daily Today and the 3-Day Outlook, plus
|
||||||
failure, print a JSON aggregate summary to stdout, write compact report status
|
Weekend Outlook except on Sunday. Run commands continue remaining reports after
|
||||||
logs to stderr, and return nonzero when any report failed. Other `generate`
|
an independent report failure, print a JSON aggregate summary to stdout, write
|
||||||
commands resolve one report request and stop before report generation.
|
compact report status logs to stderr, and return nonzero when any report
|
||||||
|
failed.
|
||||||
|
|
||||||
## Flags
|
## Flags
|
||||||
|
|
||||||
@@ -63,7 +69,7 @@ commands resolve one report request and stop before report generation.
|
|||||||
- `--config PATH`: load configuration from `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`.
|
- `--config PATH`: load configuration from `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`.
|
||||||
- `--units VALUE`: override configured Weather API units.
|
- `--units VALUE`: override configured Weather API units.
|
||||||
- `--tz NAME`: override configured Weather API timezone.
|
- `--tz NAME`: override configured Weather API timezone.
|
||||||
- `--out PATH`: optional Markdown report copy for `generate daily`, `generate tomorrow`, `generate three-day`, and `generate weekend`; reserved for later generated report output on other `generate` commands.
|
- `--out PATH`: optional Markdown report copy for `generate daily`, `generate tomorrow`, `generate three-day`, `generate weekend`, and `generate storm`.
|
||||||
- `--out-dir PATH`: optional directory for extra Markdown report copies from `run morning` and `run evening`.
|
- `--out-dir PATH`: optional directory for extra Markdown report copies from `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`; 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`.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ This document describes the implemented briefing package boundary.
|
|||||||
|
|
||||||
`internal/briefing` builds structured report-specific briefing packages from
|
`internal/briefing` builds structured report-specific briefing packages from
|
||||||
forecast summaries and report metadata. The package currently implements Daily
|
forecast summaries and report metadata. The package currently implements Daily
|
||||||
Today, Daily Tomorrow, 3-Day Outlook, and Weekend Outlook briefing content.
|
Today, Daily Tomorrow, 3-Day Outlook, Weekend Outlook, and Storm Report
|
||||||
|
briefing content.
|
||||||
|
|
||||||
## Inputs and Outputs
|
## Inputs and Outputs
|
||||||
|
|
||||||
@@ -45,6 +46,11 @@ Output:
|
|||||||
- Weekend content uses the same daily outlook summaries and adds planning
|
- Weekend content uses the same daily outlook summaries and adds planning
|
||||||
inputs for best outdoor windows, worst weather windows, rain/storm timing,
|
inputs for best outdoor windows, worst weather windows, rain/storm timing,
|
||||||
comfort concerns, and confidence or uncertainty context.
|
comfort concerns, and confidence or uncertainty context.
|
||||||
|
- Storm content uses the explicit event window and includes event headline
|
||||||
|
inputs, hazards, most-likely scenario inputs, reasonable worst-case inputs,
|
||||||
|
confidence and uncertainty inputs, watch items, active alerts, relevant
|
||||||
|
hourly and narrative forecast periods, and available discussion or weather
|
||||||
|
story context.
|
||||||
- Briefing JSON is written atomically by `briefing.Save`.
|
- Briefing JSON is written atomically by `briefing.Save`.
|
||||||
|
|
||||||
## Failure Behavior
|
## Failure Behavior
|
||||||
@@ -55,6 +61,8 @@ Output:
|
|||||||
one derived daily summary in the outlook period.
|
one derived daily summary in the outlook period.
|
||||||
- Weekend briefing construction requires a Weekend report definition and at
|
- Weekend briefing construction requires a Weekend report definition and at
|
||||||
least one derived daily summary in the weekend period.
|
least one derived daily summary in the weekend period.
|
||||||
|
- Storm briefing construction requires a Storm Report definition and a forecast
|
||||||
|
bundle.
|
||||||
- Save failures include path and operation context.
|
- Save failures include path and operation context.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
@@ -64,6 +72,7 @@ Inspect:
|
|||||||
- `internal/briefing/daily_test.go`
|
- `internal/briefing/daily_test.go`
|
||||||
- `internal/briefing/three_day_test.go`
|
- `internal/briefing/three_day_test.go`
|
||||||
- `internal/briefing/weekend_test.go`
|
- `internal/briefing/weekend_test.go`
|
||||||
|
- `internal/briefing/storm_test.go`
|
||||||
- `internal/app/app_test.go`
|
- `internal/app/app_test.go`
|
||||||
- `internal/cli/root_test.go`
|
- `internal/cli/root_test.go`
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ This document describes the implemented prompt input package boundary.
|
|||||||
|
|
||||||
Input:
|
Input:
|
||||||
|
|
||||||
- `briefing.Package` containing Daily-family, 3-Day Outlook, or Weekend Outlook
|
- `briefing.Package` containing Daily-family, 3-Day Outlook, Weekend Outlook,
|
||||||
content
|
or Storm Report content
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ weatherreporter generate daily --date 2026-05-29
|
|||||||
weatherreporter generate tomorrow
|
weatherreporter generate tomorrow
|
||||||
weatherreporter generate three-day
|
weatherreporter generate three-day
|
||||||
weatherreporter generate weekend
|
weatherreporter generate weekend
|
||||||
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
||||||
weatherreporter run morning
|
weatherreporter run morning
|
||||||
weatherreporter run evening
|
weatherreporter run evening
|
||||||
```
|
```
|
||||||
@@ -18,7 +19,8 @@ period, build the prompt input data package, run `scriptorium render`, run
|
|||||||
`scriptorium run`, and write inspectable artifacts under the configured
|
`scriptorium run`, and write inspectable artifacts under the configured
|
||||||
workspace. The evening run resolves only the Tomorrow Planning Brief. The
|
workspace. The evening run resolves only the Tomorrow Planning Brief. The
|
||||||
morning run generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
|
morning run generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
|
||||||
except on Sunday.
|
except on Sunday. Storm Report generation is manual and uses the explicit
|
||||||
|
`--start` and `--end` bounds as its valid period.
|
||||||
|
|
||||||
Scheduled run commands print a JSON aggregate summary to stdout and compact
|
Scheduled run commands print a JSON aggregate summary to stdout and compact
|
||||||
per-report status lines to stderr. If one report fails, remaining independent
|
per-report status lines to stderr. If one report fails, remaining independent
|
||||||
@@ -44,6 +46,10 @@ workspace/
|
|||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.briefing.json
|
<run_id>.briefing.json
|
||||||
<run_id>.metadata.json
|
<run_id>.metadata.json
|
||||||
|
storm/
|
||||||
|
YYYY-MM-DD/
|
||||||
|
<run_id>.briefing.json
|
||||||
|
<run_id>.metadata.json
|
||||||
data-packages/
|
data-packages/
|
||||||
daily/
|
daily/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
@@ -54,6 +60,9 @@ workspace/
|
|||||||
weekend/
|
weekend/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.data_package.json
|
<run_id>.data_package.json
|
||||||
|
storm/
|
||||||
|
YYYY-MM-DD/
|
||||||
|
<run_id>.data_package.json
|
||||||
preflight/
|
preflight/
|
||||||
daily/
|
daily/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
@@ -64,6 +73,9 @@ workspace/
|
|||||||
weekend/
|
weekend/
|
||||||
YYYY-MM-DD/
|
YYYY-MM-DD/
|
||||||
<run_id>.render.json
|
<run_id>.render.json
|
||||||
|
storm/
|
||||||
|
YYYY-MM-DD/
|
||||||
|
<run_id>.render.json
|
||||||
reports/
|
reports/
|
||||||
daily/
|
daily/
|
||||||
<run_id>.md
|
<run_id>.md
|
||||||
@@ -71,11 +83,14 @@ workspace/
|
|||||||
<run_id>.md
|
<run_id>.md
|
||||||
weekend/
|
weekend/
|
||||||
<run_id>.md
|
<run_id>.md
|
||||||
|
storm/
|
||||||
|
<run_id>.md
|
||||||
```
|
```
|
||||||
|
|
||||||
The Markdown report is written to a RunID-managed report path. When `--out` is
|
The Markdown report is written to a RunID-managed report path. When `--out` is
|
||||||
provided to `generate daily`, `generate tomorrow`, `generate three-day`, or
|
provided to `generate daily`, `generate tomorrow`, `generate three-day`,
|
||||||
`generate weekend`, the managed report is also copied to that path.
|
`generate weekend`, or `generate storm`, the managed report is also copied to
|
||||||
|
that path.
|
||||||
|
|
||||||
For `run morning` and `run evening`, `--out-dir PATH` writes extra Markdown
|
For `run morning` and `run evening`, `--out-dir PATH` writes extra Markdown
|
||||||
copies using each report definition's default filename, such as `daily.md`,
|
copies using each report definition's default filename, such as `daily.md`,
|
||||||
@@ -123,6 +138,9 @@ Weekend Outlook generation compares against a prior compatible Weekend briefing
|
|||||||
snapshot for the same weekend window when one exists. Friday evening and
|
snapshot for the same weekend window when one exists. Friday evening and
|
||||||
Saturday runs may narrow the valid start while keeping the same Monday endpoint.
|
Saturday runs may narrow the valid start while keeping the same Monday endpoint.
|
||||||
|
|
||||||
|
Storm Report generation currently leaves Recent Changes empty. Its explicit
|
||||||
|
event window is still recorded in briefing and metadata artifacts.
|
||||||
|
|
||||||
When no prior comparable snapshot exists, or no configured threshold is crossed,
|
When no prior comparable snapshot exists, or no configured threshold is crossed,
|
||||||
the Recent Changes list is empty.
|
the Recent Changes list is empty.
|
||||||
|
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ func batchOutputPath(outputDir string, definition report.Definition) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isGeneratedReport(id report.ID) bool {
|
func isGeneratedReport(id report.ID) bool {
|
||||||
return isDailyReport(id) || id == report.ThreeDay || id == report.Weekend
|
return isDailyReport(id) || id == report.ThreeDay || id == report.Weekend || id == report.Storm
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDailyReport(id report.ID) bool {
|
func isDailyReport(id report.ID) bool {
|
||||||
@@ -572,6 +572,13 @@ func BuildBriefing(req BriefingRequest, bundle *forecast.Bundle) (briefing.Packa
|
|||||||
Units: req.Config.WeatherAPI.Units,
|
Units: req.Config.WeatherAPI.Units,
|
||||||
Timezone: req.Config.WeatherAPI.Timezone,
|
Timezone: req.Config.WeatherAPI.Timezone,
|
||||||
}, summaries)
|
}, summaries)
|
||||||
|
case report.Storm:
|
||||||
|
return briefing.BuildStorm(briefing.BuildContext{
|
||||||
|
Resolved: req.Resolved,
|
||||||
|
Bundle: bundle,
|
||||||
|
Units: req.Config.WeatherAPI.Units,
|
||||||
|
Timezone: req.Config.WeatherAPI.Timezone,
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
return briefing.Package{}, fmt.Errorf("briefing is not implemented for report %q", req.Resolved.Definition.ID)
|
return briefing.Package{}, fmt.Errorf("briefing is not implemented for report %q", req.Resolved.Definition.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -673,6 +673,59 @@ func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateStormReportWritesReport(t *testing.T) {
|
||||||
|
server := dailyBundleServer(t)
|
||||||
|
cfg := config.Defaults()
|
||||||
|
cfg.WeatherAPI.BaseURL = server.URL + "/"
|
||||||
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||||
|
cfg.Workspace.Root = t.TempDir()
|
||||||
|
resolved, err := ResolveGenerate(GenerateRequest{
|
||||||
|
Config: cfg,
|
||||||
|
Report: ReportStorm,
|
||||||
|
StormStart: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
StormEnd: mustParse("2026-05-29T10:00:00-05:00"),
|
||||||
|
}, mustParse("2026-05-29T05:00:00-05:00"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||||
|
}
|
||||||
|
renderer := &recordingRenderer{
|
||||||
|
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||||
|
runResult: &scriptorium.RunResult{ExitCode: 0},
|
||||||
|
runBody: "# Storm Report\n",
|
||||||
|
}
|
||||||
|
outputPath := filepath.Join(t.TempDir(), "storm.md")
|
||||||
|
|
||||||
|
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||||
|
Config: cfg,
|
||||||
|
Resolved: resolved,
|
||||||
|
OutputPath: outputPath,
|
||||||
|
Renderer: renderer,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateReport() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Briefing.Storm == nil {
|
||||||
|
t.Fatal("Storm = nil")
|
||||||
|
}
|
||||||
|
if renderer.renderRequest.PromptID != "weather.storm_report" {
|
||||||
|
t.Fatalf("render PromptID = %q, want weather.storm_report", renderer.renderRequest.PromptID)
|
||||||
|
}
|
||||||
|
if len(result.Briefing.Storm.HourlyPeriods) == 0 || len(result.Briefing.Storm.NarrativePeriods) == 0 {
|
||||||
|
t.Fatalf("selected source periods hourly/narrative = %d/%d, want relevant periods", len(result.Briefing.Storm.HourlyPeriods), len(result.Briefing.Storm.NarrativePeriods))
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(outputPath); err != nil {
|
||||||
|
t.Fatalf("expected requested report output %q: %v", outputPath, err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(result.DataPackagePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read data package: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `"storm"`) || !strings.Contains(string(data), `"weather.storm_report"`) {
|
||||||
|
t.Fatalf("data package missing storm content:\n%s", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
||||||
cfg := config.Defaults()
|
cfg := config.Defaults()
|
||||||
cfg.WeatherAPI.Timezone = "America/Chicago"
|
cfg.WeatherAPI.Timezone = "America/Chicago"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type Package struct {
|
|||||||
Daily *Daily `json:"daily,omitempty"`
|
Daily *Daily `json:"daily,omitempty"`
|
||||||
ThreeDay *ThreeDay `json:"threeDay,omitempty"`
|
ThreeDay *ThreeDay `json:"threeDay,omitempty"`
|
||||||
Weekend *Weekend `json:"weekend,omitempty"`
|
Weekend *Weekend `json:"weekend,omitempty"`
|
||||||
|
Storm *Storm `json:"storm,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
|||||||
200
internal/briefing/storm.go
Normal file
200
internal/briefing/storm.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package briefing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storm struct {
|
||||||
|
TimingWindow timeutil.Period `json:"timingWindow"`
|
||||||
|
EventHeadlines []string `json:"eventHeadlines,omitempty"`
|
||||||
|
Hazards []string `json:"hazards,omitempty"`
|
||||||
|
MostLikelyScenario []string `json:"mostLikelyScenario,omitempty"`
|
||||||
|
ReasonableWorstCase []string `json:"reasonableWorstCase,omitempty"`
|
||||||
|
ConfidenceInputs []string `json:"confidenceInputs,omitempty"`
|
||||||
|
WhatToWatchNext []string `json:"whatToWatchNext,omitempty"`
|
||||||
|
RelevantAlerts []forecast.AlertOverlap `json:"relevantAlerts,omitempty"`
|
||||||
|
HourlyPeriods []forecast.ForecastPeriod `json:"hourlyPeriods,omitempty"`
|
||||||
|
DailyPeriods []forecast.ForecastPeriod `json:"dailyPeriods,omitempty"`
|
||||||
|
NarrativePeriods []forecast.ForecastPeriod `json:"narrativePeriods,omitempty"`
|
||||||
|
WindowSummary forecast.DaypartSummary `json:"windowSummary"`
|
||||||
|
Discussion DiscussionContext `json:"discussion,omitempty"`
|
||||||
|
WeatherStory *WeatherStoryContext `json:"weatherStory,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildStorm(ctx BuildContext) (Package, error) {
|
||||||
|
if ctx.Resolved.Definition.ID != report.Storm {
|
||||||
|
return Package{}, fmt.Errorf("storm briefing requires a storm report definition")
|
||||||
|
}
|
||||||
|
if ctx.Bundle == nil {
|
||||||
|
return Package{}, fmt.Errorf("forecast bundle is required")
|
||||||
|
}
|
||||||
|
period := ctx.Resolved.ValidPeriod
|
||||||
|
hourly := forecast.SelectHourlyPeriods(ctx.Bundle.Hourly, period)
|
||||||
|
narrative := forecast.SelectNarrativePeriods(ctx.Bundle, period)
|
||||||
|
daily := forecast.SelectHourlyPeriods(ctx.Bundle.Daily, period)
|
||||||
|
alerts := forecast.AlertOverlaps(ctx.Bundle.Alerts, period)
|
||||||
|
summary := forecast.SummarizeDaypart("storm window", period, hourly)
|
||||||
|
summary.AlertOverlaps = alerts
|
||||||
|
|
||||||
|
storm := &Storm{
|
||||||
|
TimingWindow: period,
|
||||||
|
EventHeadlines: stormHeadlines(alerts),
|
||||||
|
Hazards: stormHazards(alerts, summary),
|
||||||
|
MostLikelyScenario: mostLikelyStormScenario(hourly, narrative, summary),
|
||||||
|
ReasonableWorstCase: reasonableWorstCase(alerts, summary),
|
||||||
|
ConfidenceInputs: stormConfidenceInputs(ctx.Bundle),
|
||||||
|
WhatToWatchNext: stormWatchItems(alerts, summary, ctx.Bundle),
|
||||||
|
RelevantAlerts: alerts,
|
||||||
|
HourlyPeriods: hourly,
|
||||||
|
DailyPeriods: daily,
|
||||||
|
NarrativePeriods: narrative,
|
||||||
|
WindowSummary: summary,
|
||||||
|
Discussion: buildDiscussion(ctx.Bundle.Discussion),
|
||||||
|
WeatherStory: buildWeatherStory(ctx.Bundle),
|
||||||
|
}
|
||||||
|
return Package{
|
||||||
|
Metadata: BuildMetadata(ctx),
|
||||||
|
Storm: storm,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stormHeadlines(alerts []forecast.AlertOverlap) []string {
|
||||||
|
var headlines []string
|
||||||
|
for _, alert := range alerts {
|
||||||
|
if alert.Headline != "" {
|
||||||
|
headlines = appendUnique(headlines, alert.Headline)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if alert.Event != "" {
|
||||||
|
headlines = appendUnique(headlines, alert.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(headlines) == 0 {
|
||||||
|
return []string{"No active alert headline overlaps the selected storm window."}
|
||||||
|
}
|
||||||
|
return headlines
|
||||||
|
}
|
||||||
|
|
||||||
|
func stormHazards(alerts []forecast.AlertOverlap, summary forecast.DaypartSummary) []string {
|
||||||
|
hazards := map[string]struct{}{}
|
||||||
|
for _, alert := range alerts {
|
||||||
|
if alert.Event != "" {
|
||||||
|
hazards[alert.Event] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, hazard := range hazardsForIndicators(summary.Indicators) {
|
||||||
|
hazards[hazard] = struct{}{}
|
||||||
|
}
|
||||||
|
if summary.MaxPrecipitationProbability != nil && summary.MaxPrecipitationProbability.Value >= 50 {
|
||||||
|
hazards["precipitation"] = struct{}{}
|
||||||
|
}
|
||||||
|
if summary.PeakWindGust != nil && summary.PeakWindGust.Value >= 30 {
|
||||||
|
hazards["wind"] = struct{}{}
|
||||||
|
}
|
||||||
|
out := sortedSet(hazards)
|
||||||
|
if len(out) == 0 {
|
||||||
|
return []string{"No storm-specific hazard signal stands out in the selected source data."}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mostLikelyStormScenario(hourly []forecast.ForecastPeriod, narrative []forecast.ForecastPeriod, summary forecast.DaypartSummary) []string {
|
||||||
|
var items []string
|
||||||
|
if summary.DominantCondition != "" {
|
||||||
|
items = append(items, "Dominant hourly condition: "+summary.DominantCondition+".")
|
||||||
|
}
|
||||||
|
if summary.MaxPrecipitationProbability != nil {
|
||||||
|
items = append(items, fmt.Sprintf("Peak precipitation chance is near %.0f%% around %s.", summary.MaxPrecipitationProbability.Value, summary.MaxPrecipitationProbability.Time.Format("15:04")))
|
||||||
|
}
|
||||||
|
if summary.PeakWindGust != nil {
|
||||||
|
items = append(items, fmt.Sprintf("Peak wind gust is near %.0f mph around %s.", summary.PeakWindGust.Value, summary.PeakWindGust.Time.Format("15:04")))
|
||||||
|
}
|
||||||
|
for _, period := range narrative {
|
||||||
|
if period.TextDescription != "" {
|
||||||
|
items = append(items, "Narrative guidance: "+period.TextDescription)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 && len(hourly) > 0 {
|
||||||
|
items = append(items, "Hourly forecast periods are available, but no focused storm signal is prominent.")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = append(items, "No active storm signal is evident from the selected forecast window.")
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func reasonableWorstCase(alerts []forecast.AlertOverlap, summary forecast.DaypartSummary) []string {
|
||||||
|
var items []string
|
||||||
|
for _, alert := range alerts {
|
||||||
|
label := alert.Event
|
||||||
|
if label == "" {
|
||||||
|
label = alert.Headline
|
||||||
|
}
|
||||||
|
if label != "" {
|
||||||
|
items = appendUnique(items, "Alert scenario to consider: "+label+".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if summary.Indicators.Thunder {
|
||||||
|
items = appendUnique(items, "Thunderstorm timing or intensity could be more disruptive than the baseline forecast.")
|
||||||
|
}
|
||||||
|
if summary.Indicators.Wind {
|
||||||
|
items = appendUnique(items, "Wind impacts could be higher where stronger gusts occur.")
|
||||||
|
}
|
||||||
|
if summary.Indicators.Snow || summary.Indicators.Ice {
|
||||||
|
items = appendUnique(items, "Wintry precipitation could create travel impacts if it overlaps the event window.")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = append(items, "No clear reasonable worst-case signal is represented in the selected data.")
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func stormConfidenceInputs(bundle *forecast.Bundle) []string {
|
||||||
|
var items []string
|
||||||
|
if bundle == nil {
|
||||||
|
return []string{"No source bundle was available for confidence context."}
|
||||||
|
}
|
||||||
|
if bundle.Discussion != nil {
|
||||||
|
items = appendUnique(items, bundle.Discussion.KeyMessages...)
|
||||||
|
if bundle.Discussion.ShortTerm != nil && bundle.Discussion.ShortTerm.Narrative != "" {
|
||||||
|
items = appendUnique(items, "Short-term discussion is available for confidence context.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bundle.WeatherStory != nil && len(bundle.WeatherStory.Raw) > 0 {
|
||||||
|
items = appendUnique(items, "Weather story source is available.")
|
||||||
|
}
|
||||||
|
for _, warning := range bundle.Warnings {
|
||||||
|
if warning.Code != "" {
|
||||||
|
items = appendUnique(items, "Source warning: "+warning.Code+".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = append(items, "No explicit confidence or uncertainty signal was available from the selected source context.")
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func stormWatchItems(alerts []forecast.AlertOverlap, summary forecast.DaypartSummary, bundle *forecast.Bundle) []string {
|
||||||
|
var items []string
|
||||||
|
if len(alerts) > 0 {
|
||||||
|
items = append(items, "Watch for alert extensions, cancellations, or upgrades.")
|
||||||
|
}
|
||||||
|
if summary.MaxPrecipitationProbability != nil {
|
||||||
|
items = append(items, "Watch precipitation timing and probability trends.")
|
||||||
|
}
|
||||||
|
if summary.PeakWindGust != nil {
|
||||||
|
items = append(items, "Watch wind gust trends.")
|
||||||
|
}
|
||||||
|
if bundle != nil && bundle.Discussion != nil {
|
||||||
|
items = append(items, "Watch the next forecast discussion update for confidence changes.")
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
items = append(items, "Watch for new alerts or stronger wording if the weather pattern changes.")
|
||||||
|
}
|
||||||
|
return appendUnique(nil, items...)
|
||||||
|
}
|
||||||
147
internal/briefing/storm_test.go
Normal file
147
internal/briefing/storm_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package briefing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStormBriefingWithActiveAlert(t *testing.T) {
|
||||||
|
location := mustLocation(t)
|
||||||
|
resolved, err := report.Resolve(report.Storm, report.ResolveRequest{
|
||||||
|
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||||
|
Location: location,
|
||||||
|
StormStart: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
StormEnd: mustParse("2026-05-29T12:00:00-05:00"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve storm: %v", err)
|
||||||
|
}
|
||||||
|
precip := 80.0
|
||||||
|
gust := 42.0
|
||||||
|
bundle := &forecast.Bundle{
|
||||||
|
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||||
|
StartTime: mustParse("2026-05-29T07:00:00-05:00"),
|
||||||
|
EndTime: mustParse("2026-05-29T08:00:00-05:00"),
|
||||||
|
TextDescription: "Severe thunderstorms and gusty wind",
|
||||||
|
ProbabilityOfPrecipitationPercent: &precip,
|
||||||
|
WindGustMph: &gust,
|
||||||
|
}}},
|
||||||
|
Daily: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||||
|
StartTime: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
EndTime: mustParse("2026-05-29T18:00:00-05:00"),
|
||||||
|
TextDescription: "Storms likely.",
|
||||||
|
}}},
|
||||||
|
Narrative: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{
|
||||||
|
StartTime: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
EndTime: mustParse("2026-05-29T18:00:00-05:00"),
|
||||||
|
TextDescription: "Damaging wind possible in stronger storms.",
|
||||||
|
}}},
|
||||||
|
Alerts: &forecast.AlertRun{Alerts: []json.RawMessage{
|
||||||
|
json.RawMessage(`{"event":"Severe Thunderstorm Warning","headline":"Severe storms near Testville","severity":"Severe","effective":"2026-05-29T06:30:00-05:00","expires":"2026-05-29T08:30:00-05:00"}`),
|
||||||
|
}},
|
||||||
|
Discussion: &forecast.Discussion{Product: "discussion", KeyMessages: []string{"Storms may intensify quickly."}},
|
||||||
|
WeatherStory: &forecast.WeatherStory{Raw: json.RawMessage(`{"headline":"Storm risk"}`)},
|
||||||
|
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildStorm() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkg.Metadata.ReportID != report.Storm {
|
||||||
|
t.Fatalf("ReportID = %q, want storm", pkg.Metadata.ReportID)
|
||||||
|
}
|
||||||
|
if pkg.Storm == nil {
|
||||||
|
t.Fatal("Storm = nil")
|
||||||
|
}
|
||||||
|
if len(pkg.Storm.RelevantAlerts) != 1 || len(pkg.Storm.EventHeadlines) != 1 {
|
||||||
|
t.Fatalf("alerts/headlines = %#v/%#v, want alert inputs", pkg.Storm.RelevantAlerts, pkg.Storm.EventHeadlines)
|
||||||
|
}
|
||||||
|
if !pkg.Storm.TimingWindow.Start.Equal(resolved.ValidPeriod.Start) || !pkg.Storm.TimingWindow.End.Equal(resolved.ValidPeriod.End) {
|
||||||
|
t.Fatalf("TimingWindow = %#v, want resolved valid period %#v", pkg.Storm.TimingWindow, resolved.ValidPeriod)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(pkg.Storm.Hazards, ","), "Severe Thunderstorm Warning") {
|
||||||
|
t.Fatalf("Hazards = %#v, want alert event", pkg.Storm.Hazards)
|
||||||
|
}
|
||||||
|
if len(pkg.Storm.HourlyPeriods) != 1 || len(pkg.Storm.DailyPeriods) != 1 || len(pkg.Storm.NarrativePeriods) != 1 {
|
||||||
|
t.Fatalf("selected periods hourly/daily/narrative = %d/%d/%d, want selected source periods", len(pkg.Storm.HourlyPeriods), len(pkg.Storm.DailyPeriods), len(pkg.Storm.NarrativePeriods))
|
||||||
|
}
|
||||||
|
if pkg.Storm.WeatherStory == nil {
|
||||||
|
t.Fatal("WeatherStory = nil, want available story context")
|
||||||
|
}
|
||||||
|
if len(pkg.Storm.WhatToWatchNext) == 0 {
|
||||||
|
t.Fatal("WhatToWatchNext length = 0, want watch inputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStormBriefingWithDiscussionButNoAlert(t *testing.T) {
|
||||||
|
location := mustLocation(t)
|
||||||
|
resolved, err := report.Resolve(report.Storm, report.ResolveRequest{
|
||||||
|
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||||
|
Location: location,
|
||||||
|
StormStart: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
StormEnd: mustParse("2026-05-29T12:00:00-05:00"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve storm: %v", err)
|
||||||
|
}
|
||||||
|
bundle := &forecast.Bundle{
|
||||||
|
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Showers"}}},
|
||||||
|
Alerts: &forecast.AlertRun{},
|
||||||
|
Discussion: &forecast.Discussion{Product: "discussion", KeyMessages: []string{"Confidence is moderate."}},
|
||||||
|
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildStorm() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pkg.Storm.RelevantAlerts) != 0 {
|
||||||
|
t.Fatalf("RelevantAlerts length = %d, want 0", len(pkg.Storm.RelevantAlerts))
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(pkg.Storm.EventHeadlines, " "), "No active alert") {
|
||||||
|
t.Fatalf("EventHeadlines = %#v, want no-alert fallback", pkg.Storm.EventHeadlines)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(pkg.Storm.ConfidenceInputs, " "), "Confidence is moderate") {
|
||||||
|
t.Fatalf("ConfidenceInputs = %#v, want discussion key message", pkg.Storm.ConfidenceInputs)
|
||||||
|
}
|
||||||
|
if len(pkg.Storm.MostLikelyScenario) == 0 {
|
||||||
|
t.Fatal("MostLikelyScenario length = 0, want forecast scenario inputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStormBriefingQuietWindow(t *testing.T) {
|
||||||
|
location := mustLocation(t)
|
||||||
|
resolved, err := report.Resolve(report.Storm, report.ResolveRequest{
|
||||||
|
Now: mustParse("2026-05-29T05:00:00-05:00"),
|
||||||
|
Location: location,
|
||||||
|
StormStart: mustParse("2026-05-29T06:00:00-05:00"),
|
||||||
|
StormEnd: mustParse("2026-05-29T12:00:00-05:00"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve storm: %v", err)
|
||||||
|
}
|
||||||
|
bundle := &forecast.Bundle{
|
||||||
|
Hourly: &forecast.ForecastRun{Periods: []forecast.ForecastPeriod{{StartTime: mustParse("2026-05-29T07:00:00-05:00"), EndTime: mustParse("2026-05-29T08:00:00-05:00"), TextDescription: "Clear"}}},
|
||||||
|
Sources: []forecast.Source{{Name: "hourly", FetchedAt: time.Now()}},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := BuildStorm(BuildContext{Resolved: resolved, Bundle: bundle, Units: "us", Timezone: "America/Chicago"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildStorm() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pkg.Storm.Hazards) != 1 || !strings.Contains(pkg.Storm.Hazards[0], "No storm-specific") {
|
||||||
|
t.Fatalf("Hazards = %#v, want quiet hazard fallback", pkg.Storm.Hazards)
|
||||||
|
}
|
||||||
|
if !strings.Contains(strings.Join(pkg.Storm.WhatToWatchNext, " "), "new alerts") {
|
||||||
|
t.Fatalf("WhatToWatchNext = %#v, want watch fallback", pkg.Storm.WhatToWatchNext)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,17 +58,51 @@ func TestRunUnknownCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunGenerateReturnsNotImplementedAfterResolution(t *testing.T) {
|
func TestRunGenerateStormWritesMarkdownReport(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)
|
||||||
|
}
|
||||||
|
outPath := filepath.Join(tempDir, "storm.md")
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
runner := Runner{Clock: fixedClock()}
|
runner := Runner{Clock: fixedClock()}
|
||||||
|
|
||||||
err := runner.Run(context.Background(), []string{"generate", "storm", "--units", "metric", "--start", "2026-05-29T18:00", "--end", "2026-05-30T06:00"}, &stdout, &stderr)
|
err := runner.Run(context.Background(), []string{
|
||||||
if err == nil {
|
"generate", "storm",
|
||||||
t.Fatal("Run() error = nil, want app not implemented error")
|
"--config", configPath,
|
||||||
|
"--start", "2026-05-29T06:00",
|
||||||
|
"--end", "2026-05-29T10:00",
|
||||||
|
"--out", outPath,
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Run() error = %v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "generate is not implemented") {
|
report, err := os.ReadFile(outPath)
|
||||||
t.Fatalf("Run() error = %q, want app not implemented error", err.Error())
|
if err != nil {
|
||||||
|
t.Fatalf("read report: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(report), "# Daily Report") {
|
||||||
|
t.Fatalf("report output missing markdown:\n%s", string(report))
|
||||||
|
}
|
||||||
|
dataPackageMatches, err := filepath.Glob(filepath.Join(workspaceRoot, "data-packages", "storm", "2026-05-29", "*.data_package.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("glob data package: %v", err)
|
||||||
|
}
|
||||||
|
if len(dataPackageMatches) != 1 {
|
||||||
|
t.Fatalf("data package files = %#v, want one", dataPackageMatches)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(dataPackageMatches[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read managed data package: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), `"storm"`) || !strings.Contains(string(data), `"weather.storm_report"`) {
|
||||||
|
t.Fatalf("data package output missing storm content:\n%s", string(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ func Validate(pkg Package) error {
|
|||||||
if pkg.Briefing.Metadata.ReportID != pkg.Report.ID {
|
if pkg.Briefing.Metadata.ReportID != pkg.Report.ID {
|
||||||
return fmt.Errorf("briefing.metadata.reportId must match report.id")
|
return fmt.Errorf("briefing.metadata.reportId must match report.id")
|
||||||
}
|
}
|
||||||
if pkg.Briefing.Daily == nil && pkg.Briefing.ThreeDay == nil && pkg.Briefing.Weekend == nil {
|
if pkg.Briefing.Daily == nil && pkg.Briefing.ThreeDay == nil && pkg.Briefing.Weekend == nil && pkg.Briefing.Storm == nil {
|
||||||
return fmt.Errorf("briefing report content is required")
|
return fmt.Errorf("briefing report content is required")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -98,6 +98,33 @@ func TestBuildWeekendDataPackage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildStormDataPackage(t *testing.T) {
|
||||||
|
briefingPackage := validBriefingPackage()
|
||||||
|
briefingPackage.Metadata.RunID = "20260529T100000Z_storm"
|
||||||
|
briefingPackage.Metadata.ReportID = report.Storm
|
||||||
|
briefingPackage.Metadata.PromptID = "weather.storm_report"
|
||||||
|
briefingPackage.Daily = nil
|
||||||
|
briefingPackage.Storm = &briefing.Storm{
|
||||||
|
TimingWindow: briefingPackage.Metadata.ValidPeriod,
|
||||||
|
Hazards: []string{"Thunderstorms"},
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := Build(briefingPackage)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkg.Report.ID != report.Storm {
|
||||||
|
t.Fatalf("Report.ID = %q, want storm", pkg.Report.ID)
|
||||||
|
}
|
||||||
|
if pkg.Report.PromptID != "weather.storm_report" {
|
||||||
|
t.Fatalf("PromptID = %q, want weather.storm_report", pkg.Report.PromptID)
|
||||||
|
}
|
||||||
|
if pkg.Briefing.Storm == nil {
|
||||||
|
t.Fatal("Briefing.Storm = nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalDeterministic(t *testing.T) {
|
func TestMarshalDeterministic(t *testing.T) {
|
||||||
pkg, err := Build(validBriefingPackage())
|
pkg, err := Build(validBriefingPackage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user