Add hourly generated-text report shell
This commit is contained in:
21
docs/cli.md
21
docs/cli.md
@@ -35,14 +35,14 @@ weatherreporter inspect prior [--config PATH] RUN_ID
|
|||||||
weatherreporter inspect sources [--config PATH] RUN_ID
|
weatherreporter inspect sources [--config PATH] RUN_ID
|
||||||
```
|
```
|
||||||
|
|
||||||
`generate` commands write a JSON module snapshot, YAML data package, preflight
|
Implemented Markdown `generate` commands write a JSON module snapshot, YAML
|
||||||
artifact, managed Markdown report, and metadata under the configured workspace.
|
data package, preflight artifact, managed Markdown report, and metadata under
|
||||||
`--out` writes an extra Markdown copy for the operator; distributor
|
the configured workspace. `--out` writes an extra Markdown copy for the
|
||||||
notification uses the managed report path, not the extra copy. `generate
|
operator; distributor notification uses the managed report path, not the extra
|
||||||
hourly` uses the current generation time, covers the next six hours in the
|
copy. `generate hourly` is parsed and resolved, covers the next six hours in
|
||||||
effective report timezone, and does not accept date or event window flags.
|
the effective report timezone, and does not accept date or event window flags;
|
||||||
`generate storm` requires explicit event-window bounds with `--start` and
|
its rendering workflow is not implemented yet. `generate storm` requires
|
||||||
`--end`.
|
explicit event-window bounds with `--start` and `--end`.
|
||||||
|
|
||||||
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
|
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
|
||||||
except on Sunday. `run evening` generates the Tomorrow Planning Brief. Batch
|
except on Sunday. `run evening` generates the Tomorrow Planning Brief. Batch
|
||||||
@@ -54,8 +54,8 @@ notification is enabled, batch summaries and status lines include notification
|
|||||||
status, accepted distributor run ID, or notification error fields for each
|
status, accepted distributor run ID, or notification error fields for each
|
||||||
attempted report.
|
attempted report.
|
||||||
|
|
||||||
Hourly Report generation is explicit only; it is not included in `run
|
Hourly Report is explicit only; it is not included in `run morning` or `run
|
||||||
morning` or `run evening`.
|
evening`.
|
||||||
|
|
||||||
`inspect` commands read existing workspace artifacts and emit JSON to stdout.
|
`inspect` commands read existing workspace artifacts and emit JSON to stdout.
|
||||||
They do not fetch weather data or invoke `scriptorium`.
|
They do not fetch weather data or invoke `scriptorium`.
|
||||||
@@ -83,7 +83,6 @@ are no distributor-specific CLI flags.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
weatherreporter generate tomorrow --out ./tomorrow.md
|
weatherreporter generate tomorrow --out ./tomorrow.md
|
||||||
weatherreporter generate hourly --out ./hourly.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
|
weatherreporter generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00 --out ./storm.md
|
||||||
|
|||||||
@@ -68,11 +68,12 @@ Prompt-facing module intervals use local `period_begins` and `period_ends`
|
|||||||
labels; canonical report metadata and source timestamps remain structured
|
labels; canonical report metadata and source timestamps remain structured
|
||||||
timestamps where applicable.
|
timestamps where applicable.
|
||||||
|
|
||||||
Hourly Report uses the same package schema and categories. Its default
|
Hourly Report module snapshots use the same package schema and categories when
|
||||||
package includes `precip_timing` under `derived_summaries`, alert and SPC
|
converted into prompt input. The default hourly module list places
|
||||||
outlooks under `applicable_risk_products`, AFD/SPC discussion/weather story
|
`precip_timing` under `derived_summaries`, alert and SPC outlooks under
|
||||||
under `narrative_products`, and current/hourly data under `raw_data`. It does
|
`applicable_risk_products`, AFD/SPC discussion/weather story under
|
||||||
not include daily or daypart summary stanzas.
|
`narrative_products`, and current/hourly data under `raw_data`. It does not
|
||||||
|
include daily or daypart summary stanzas.
|
||||||
|
|
||||||
Current categories are:
|
Current categories are:
|
||||||
|
|
||||||
@@ -112,7 +113,6 @@ None.
|
|||||||
## Skip And Resume Behavior
|
## Skip And Resume Behavior
|
||||||
|
|
||||||
None. Recent Changes is always present as an `items` list and may be empty.
|
None. Recent Changes is always present as an `items` list and may be empty.
|
||||||
Hourly Report currently writes an empty `items` list.
|
|
||||||
|
|
||||||
## Failure Behavior
|
## Failure Behavior
|
||||||
|
|
||||||
|
|||||||
@@ -26,20 +26,21 @@ Each report definition declares:
|
|||||||
- morning or evening batch membership
|
- morning or evening batch membership
|
||||||
- default ordered module composition
|
- default ordered module composition
|
||||||
|
|
||||||
Current report definitions use the `scriptorium_markdown` generation mode.
|
Current Markdown report definitions use the `scriptorium_markdown` generation
|
||||||
Template and structured-text schema identifiers are empty for these Markdown
|
mode. Their template and structured-text schema identifiers are empty. Hourly
|
||||||
reports.
|
Report declares `generated_text_template` with template ID `hourly` and schema
|
||||||
|
ID `hourly`; the app rendering workflow for that mode is not implemented yet.
|
||||||
|
|
||||||
## Reports
|
## Reports
|
||||||
|
|
||||||
| Report | ID | Prompt | Artifact group | Batch copy | Prior compatibility |
|
| Report | ID | Prompt | Generation mode | Artifact group | Batch copy | Prior compatibility |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| Daily Today | `daily_today` | `weather.daily_report` | `daily` | `daily.md` | Daily Today, Daily Tomorrow |
|
| Daily Today | `daily_today` | `weather.daily_report` | `scriptorium_markdown` | `daily` | `daily.md` | Daily Today, Daily Tomorrow |
|
||||||
| Daily Tomorrow | `daily_tomorrow` | `weather.daily_report` | `daily` | `tomorrow.md` | Daily Today, Daily Tomorrow |
|
| Daily Tomorrow | `daily_tomorrow` | `weather.daily_report` | `scriptorium_markdown` | `daily` | `tomorrow.md` | Daily Today, Daily Tomorrow |
|
||||||
| Hourly Report | `hourly` | `weather.hourly_report` | `hourly` | `hourly.md` | Hourly Report |
|
| Hourly Report | `hourly` | `weather.hourly_generated_text` | `generated_text_template` | `hourly` | `hourly.md` | Hourly Report |
|
||||||
| 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `three-day` | `three-day.md` | 3-Day Outlook |
|
| 3-Day Outlook | `three_day` | `weather.three_day_outlook` | `scriptorium_markdown` | `three-day` | `three-day.md` | 3-Day Outlook |
|
||||||
| Weekend Outlook | `weekend` | `weather.weekend_outlook` | `weekend` | `weekend.md` | Weekend Outlook |
|
| Weekend Outlook | `weekend` | `weather.weekend_outlook` | `scriptorium_markdown` | `weekend` | `weekend.md` | Weekend Outlook |
|
||||||
| Storm Report | `storm` | `weather.storm_report` | `storm` | `storm.md` | Storm Report |
|
| Storm Report | `storm` | `weather.storm_report` | `scriptorium_markdown` | `storm` | `storm.md` | Storm Report |
|
||||||
|
|
||||||
All report definitions are eligible for generation.
|
All report definitions are eligible for generation.
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,24 @@ Generation commands:
|
|||||||
```text
|
```text
|
||||||
weatherreporter generate daily --date 2026-05-29
|
weatherreporter generate daily --date 2026-05-29
|
||||||
weatherreporter generate tomorrow
|
weatherreporter generate tomorrow
|
||||||
weatherreporter generate hourly
|
|
||||||
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 generate storm --start 2026-05-29T18:00 --end 2026-05-30T06:00
|
||||||
```
|
```
|
||||||
|
|
||||||
Each command resolves a report period, fetches a Weather API bundle, builds a
|
Each implemented Markdown generation command resolves a report period, fetches
|
||||||
JSON module snapshot, builds a YAML prompt input data package, runs
|
a Weather API bundle, builds a JSON module snapshot, builds a YAML prompt input
|
||||||
`scriptorium render`, runs `scriptorium run`, and writes managed artifacts under
|
data package, runs `scriptorium render`, runs `scriptorium run`, and writes
|
||||||
the configured workspace.
|
managed artifacts under the configured workspace.
|
||||||
When distributor notification is enabled, weatherreporter uploads the managed
|
When distributor notification is enabled, weatherreporter uploads the managed
|
||||||
Markdown report after `scriptorium run` succeeds and final metadata is saved.
|
Markdown report after `scriptorium run` succeeds and final metadata is saved.
|
||||||
`--out PATH` writes an extra Markdown copy for the current generated report; it
|
`--out PATH` writes an extra Markdown copy for the current generated report; it
|
||||||
is not used as the distributor upload source.
|
is not used as the distributor upload source.
|
||||||
|
|
||||||
`generate hourly` is an explicit generation command. It covers the six-hour
|
`generate hourly` is an explicit command shell. It covers the six-hour rolling
|
||||||
rolling period from generation time in the effective report timezone and is not
|
period from generation time in the effective report timezone and is not part of
|
||||||
part of scheduled morning or evening batches.
|
scheduled morning or evening batches, but its rendering workflow is not
|
||||||
|
implemented yet.
|
||||||
|
|
||||||
Batch commands:
|
Batch commands:
|
||||||
|
|
||||||
|
|||||||
@@ -438,6 +438,9 @@ func FetchAndSaveBundle(ctx context.Context, req FetchBundleRequest) (*weatherda
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, error) {
|
func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, error) {
|
||||||
|
if req.Resolved.Definition.GenerationMode != report.GenerationModeScriptoriumMarkdown {
|
||||||
|
return nil, fmt.Errorf("generation mode %q is not implemented for report %q", req.Resolved.Definition.GenerationMode, req.Resolved.Definition.ID)
|
||||||
|
}
|
||||||
store := req.Store
|
store := req.Store
|
||||||
if store == nil {
|
if store == nil {
|
||||||
defaultStore, err := defaultStore(req.Config)
|
defaultStore, err := defaultStore(req.Config)
|
||||||
|
|||||||
@@ -349,9 +349,8 @@ func TestGenerateReportOmitsSPCConvectiveDiscussionBelowThreshold(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateHourlyReportWritesExpectedArtifacts(t *testing.T) {
|
func TestGenerateHourlyReportModeNotImplemented(t *testing.T) {
|
||||||
server := hourlyBundleServer(t)
|
cfg := config.Defaults()
|
||||||
cfg := dailyTestConfig(t, server)
|
|
||||||
cfg.Workspace.Root = t.TempDir()
|
cfg.Workspace.Root = t.TempDir()
|
||||||
resolved, err := ResolveGenerate(GenerateRequest{
|
resolved, err := ResolveGenerate(GenerateRequest{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@@ -360,174 +359,18 @@ func TestGenerateHourlyReportWritesExpectedArtifacts(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveGenerate() error = %v", err)
|
t.Fatalf("ResolveGenerate() error = %v", err)
|
||||||
}
|
}
|
||||||
outputPath := filepath.Join(t.TempDir(), "hourly.md")
|
if resolved.Definition.GenerationMode != report.GenerationModeGeneratedTextTemplate {
|
||||||
renderer := successfulRenderer("# Hourly Report\n")
|
t.Fatalf("GenerationMode = %q, want generated text template", resolved.Definition.GenerationMode)
|
||||||
|
}
|
||||||
|
|
||||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
_, err = GenerateReport(context.Background(), ReportRequest{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
Resolved: resolved,
|
Resolved: resolved,
|
||||||
OutputPath: outputPath,
|
Renderer: successfulRenderer("# Hourly Report\n"),
|
||||||
Renderer: renderer,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err == nil || !strings.Contains(err.Error(), `generation mode "generated_text_template" is not implemented for report "hourly"`) {
|
||||||
t.Fatalf("GenerateReport() error = %v", err)
|
t.Fatalf("GenerateReport() error = %v, want generation mode not implemented", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Metadata.ReportID != report.Hourly || result.Metadata.PromptID != "weather.hourly_report" {
|
|
||||||
t.Fatalf("metadata report/prompt = %q/%q, want hourly", result.Metadata.ReportID, result.Metadata.PromptID)
|
|
||||||
}
|
|
||||||
if got := result.Metadata.ValidPeriod.Start.Format(time.RFC3339); got != "2026-05-29T08:30:00-05:00" {
|
|
||||||
t.Fatalf("valid period start = %s, want rolling window start", got)
|
|
||||||
}
|
|
||||||
if got := result.Metadata.ValidPeriod.End.Format(time.RFC3339); got != "2026-05-29T14:30:00-05:00" {
|
|
||||||
t.Fatalf("valid period end = %s, want six-hour rolling window end", got)
|
|
||||||
}
|
|
||||||
if renderer.renderRequest.PromptID != "weather.hourly_report" || renderer.runRequest.PromptID != "weather.hourly_report" {
|
|
||||||
t.Fatalf("renderer prompt IDs = %q/%q, want hourly prompt", renderer.renderRequest.PromptID, renderer.runRequest.PromptID)
|
|
||||||
}
|
|
||||||
if !strings.Contains(result.ReportPath, filepath.Join("reports", "hourly")) {
|
|
||||||
t.Fatalf("ReportPath = %q, want hourly artifact group", result.ReportPath)
|
|
||||||
}
|
|
||||||
if !strings.Contains(result.DataPackagePath, filepath.Join("data-packages", "hourly", "2026-05-29")) {
|
|
||||||
t.Fatalf("DataPackagePath = %q, want hourly artifact group", result.DataPackagePath)
|
|
||||||
}
|
|
||||||
assertPathsExist(t, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
|
|
||||||
if result.OutputPath != outputPath {
|
|
||||||
t.Fatalf("OutputPath = %q, want requested output copy %q", result.OutputPath, outputPath)
|
|
||||||
}
|
|
||||||
copiedReport, err := os.ReadFile(outputPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read output copy: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(copiedReport), "# Hourly Report") {
|
|
||||||
t.Fatalf("output copy missing rendered report:\n%s", string(copiedReport))
|
|
||||||
}
|
|
||||||
|
|
||||||
wantModules := []module.ID{
|
|
||||||
module.Metadata,
|
|
||||||
module.CurrentConditions,
|
|
||||||
module.HourlyForecast,
|
|
||||||
module.PrecipTiming,
|
|
||||||
module.AlertDigest,
|
|
||||||
module.SPCConvectiveOutlooks,
|
|
||||||
module.AreaForecastDiscussion,
|
|
||||||
module.SPCConvectiveDiscussion,
|
|
||||||
module.WeatherStory,
|
|
||||||
}
|
|
||||||
if got := snapshotModuleIDs(result.ModuleSnapshot); strings.Join(moduleIDsForTest(got), ",") != strings.Join(moduleIDsForTest(wantModules), ",") {
|
|
||||||
t.Fatalf("module snapshot IDs = %#v, want %#v", got, wantModules)
|
|
||||||
}
|
|
||||||
|
|
||||||
hourly, ok, err := module.StanzaValue[briefing.HourlyForecastModule](result.ModuleSnapshot, "hourly_forecast")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode hourly forecast: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || len(hourly.Periods) != 5 {
|
|
||||||
t.Fatalf("hourly forecast = %#v, want five overlapping hourly periods", hourly)
|
|
||||||
}
|
|
||||||
hourlyJSON := mustMarshalString(t, hourly)
|
|
||||||
if !strings.Contains(hourlyJSON, "Showers entering the area") || !strings.Contains(hourlyJSON, "Heavy rain") {
|
|
||||||
t.Fatalf("hourly forecast missing selected hourly periods:\n%s", hourlyJSON)
|
|
||||||
}
|
|
||||||
if strings.Contains(hourlyJSON, "Before-window storms") || strings.Contains(hourlyJSON, "After-window rain") {
|
|
||||||
t.Fatalf("hourly forecast contains periods outside valid window:\n%s", hourlyJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
precip, ok, err := module.StanzaValue[briefing.PrecipTimingModule](result.ModuleSnapshot, "precip_timing")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode precip timing: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || precip.MaxPopPercent == nil || *precip.MaxPopPercent != 80 || len(precip.PrecipitationWindows) != 2 {
|
|
||||||
t.Fatalf("precip timing = %#v, want hourly precipitation windows", precip)
|
|
||||||
}
|
|
||||||
if precip.PrecipitationWindows[0].PeriodBegins != "2026-05-29 at 8:00 AM" || precip.PrecipitationWindows[0].PeriodEnds != "2026-05-29 at 9:00 AM" {
|
|
||||||
t.Fatalf("first precip window = %#v, want first selected hour only", precip.PrecipitationWindows[0])
|
|
||||||
}
|
|
||||||
if precip.PrecipitationWindows[1].PeriodBegins != "2026-05-29 at 10:00 AM" || precip.PrecipitationWindows[1].PeriodEnds != "2026-05-29 at 12:00 PM" {
|
|
||||||
t.Fatalf("second precip window = %#v, want late-morning hourly rain", precip.PrecipitationWindows[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
alerts, ok, err := module.StanzaValue[briefing.AlertDigestModule](result.ModuleSnapshot, "alert_digest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode alert digest: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || !alerts.Checked || alerts.ActiveCount != 3 || alerts.RelevantCount != 1 || len(alerts.Relevant) != 1 || alerts.Relevant[0].Event != "Flood Watch" {
|
|
||||||
t.Fatalf("alert digest = %#v, want only hourly alert overlap relevant", alerts)
|
|
||||||
}
|
|
||||||
|
|
||||||
outlooks, ok, err := module.StanzaValue[briefing.SPCConvectiveOutlooksModule](result.ModuleSnapshot, "spc_convective_outlooks")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode SPC outlooks: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || !outlooks.Checked || outlooks.OutlookCount != 1 || len(outlooks.Outlooks) != 1 || outlooks.Outlooks[0].Label != "SLGT" {
|
|
||||||
t.Fatalf("SPC outlooks = %#v, want one overlapping hourly outlook", outlooks)
|
|
||||||
}
|
|
||||||
discussion, ok, err := module.StanzaValue[briefing.SPCConvectiveDiscussionModule](result.ModuleSnapshot, "spc_convective_discussion")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode SPC discussion: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || len(discussion.Discussions) != 1 || discussion.Discussions[0].Headline != "hourly severe storms" {
|
|
||||||
t.Fatalf("SPC discussion = %#v, want discussion for retained overlapping outlook", discussion)
|
|
||||||
}
|
|
||||||
|
|
||||||
afd, ok, err := module.StanzaValue[briefing.AreaForecastDiscussionModule](result.ModuleSnapshot, "area_forecast_discussion")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode AFD: %v", err)
|
|
||||||
}
|
|
||||||
if !ok || len(afd.KeyMessages) != 1 || afd.ShortTerm != "Short-term AFD narrative for hourly report." {
|
|
||||||
t.Fatalf("AFD = %#v, want key messages and short term", afd)
|
|
||||||
}
|
|
||||||
if afd.Product != "" || afd.LongTerm != "" {
|
|
||||||
t.Fatalf("AFD = %#v, want hourly defaults to omit product and long term", afd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.PriorSnapshot != nil || len(result.RecentChanges) != 0 || len(result.DataPackage.RecentChanges.Items) != 0 {
|
|
||||||
t.Fatalf("prior=%#v recent=%#v package=%#v, want no rolling-window comparison output", result.PriorSnapshot, result.RecentChanges, result.DataPackage.RecentChanges.Items)
|
|
||||||
}
|
|
||||||
data := readDataPackageForTest(t, result)
|
|
||||||
text := string(data)
|
|
||||||
for _, want := range []string{
|
|
||||||
"id: hourly",
|
|
||||||
"prompt_id: weather.hourly_report",
|
|
||||||
"valid_period:",
|
|
||||||
"recent_changes:",
|
|
||||||
" items: []",
|
|
||||||
" alert_digest:",
|
|
||||||
" spc_convective_outlooks:",
|
|
||||||
" precip_timing:",
|
|
||||||
" area_forecast_discussion:",
|
|
||||||
" spc_convective_discussion:",
|
|
||||||
" weather_story:",
|
|
||||||
" current_conditions:",
|
|
||||||
" hourly_forecast:",
|
|
||||||
} {
|
|
||||||
if !strings.Contains(text, want) {
|
|
||||||
t.Fatalf("data package missing %q:\n%s", want, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, omitted := range []string{"validPeriod:", "Before-window storms", "After-window rain", "Evening Advisory", "Day 2 outlook", "Long-term AFD narrative for hourly report."} {
|
|
||||||
if strings.Contains(text, omitted) {
|
|
||||||
t.Fatalf("data package contains %q, want hourly filtered/canonical output:\n%s", omitted, text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
riskIndex := strings.Index(text, " applicable_risk_products:")
|
|
||||||
derivedIndex := strings.Index(text, " derived_summaries:")
|
|
||||||
narrativeIndex := strings.Index(text, " narrative_products:")
|
|
||||||
rawIndex := strings.Index(text, " raw_data:")
|
|
||||||
alertIndex := strings.Index(text, " alert_digest:")
|
|
||||||
precipIndex := strings.Index(text, " precip_timing:")
|
|
||||||
afdIndex := strings.Index(text, " area_forecast_discussion:")
|
|
||||||
currentIndex := strings.Index(text, " current_conditions:")
|
|
||||||
hourlyIndex := strings.Index(text, " hourly_forecast:")
|
|
||||||
if riskIndex < 0 || derivedIndex < 0 || narrativeIndex < 0 || rawIndex < 0 || alertIndex < 0 || precipIndex < 0 || afdIndex < 0 || currentIndex < 0 || hourlyIndex < 0 ||
|
|
||||||
!(riskIndex < alertIndex && alertIndex < derivedIndex && derivedIndex < precipIndex && precipIndex < narrativeIndex && narrativeIndex < afdIndex && afdIndex < rawIndex && rawIndex < currentIndex && currentIndex < hourlyIndex) {
|
|
||||||
t.Fatalf("data package category order is wrong:\n%s", text)
|
|
||||||
}
|
|
||||||
loaded, err := promptinput.LoadYAML(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("LoadYAML() error = %v", err)
|
|
||||||
}
|
|
||||||
assertNoStaleModuleIntervalKeys(t, loaded.Briefing.Values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
|
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
|
||||||
@@ -1367,7 +1210,7 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
|||||||
name: "hourly",
|
name: "hourly",
|
||||||
kind: ReportHourly,
|
kind: ReportHourly,
|
||||||
wantID: report.Hourly,
|
wantID: report.Hourly,
|
||||||
wantPrompt: "weather.hourly_report",
|
wantPrompt: "weather.hourly_generated_text",
|
||||||
wantStart: "2026-05-29T08:00:00-05:00",
|
wantStart: "2026-05-29T08:00:00-05:00",
|
||||||
wantEnd: "2026-05-29T14:00:00-05:00",
|
wantEnd: "2026-05-29T14:00:00-05:00",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -910,6 +910,19 @@ func TestResolveGenerateAppliesSharedFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveGenerateRejectsRetiredHourlyCommand(t *testing.T) {
|
||||||
|
runner := Runner{Clock: fixedClock()}
|
||||||
|
retired := strings.Join([]string{"near", "term"}, "-")
|
||||||
|
|
||||||
|
_, err := runner.resolveGenerate([]string{retired})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("resolveGenerate() error = nil, want unknown report")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "unknown generate report") {
|
||||||
|
t.Fatalf("error = %q, want unknown generate report", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveGenerateStormRequiresStartAndEnd(t *testing.T) {
|
func TestResolveGenerateStormRequiresStartAndEnd(t *testing.T) {
|
||||||
runner := Runner{Clock: fixedClock()}
|
runner := Runner{Clock: fixedClock()}
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,28 @@ reports:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReportModuleOverrideRejectsRetiredHourlyKeys(t *testing.T) {
|
||||||
|
for _, key := range []string{
|
||||||
|
strings.Join([]string{"near", "term"}, "_"),
|
||||||
|
strings.Join([]string{"near", "term"}, "-"),
|
||||||
|
} {
|
||||||
|
t.Run(key, func(t *testing.T) {
|
||||||
|
_, err := LoadFile(writeConfig(t, `
|
||||||
|
reports:
|
||||||
|
`+key+`:
|
||||||
|
deterministic_modules:
|
||||||
|
- metadata
|
||||||
|
`))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("LoadFile() error = nil, want unknown report")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "is not a known report") {
|
||||||
|
t.Fatalf("error = %q, want unknown report", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExplicitMissingConfigReturnsError(t *testing.T) {
|
func TestExplicitMissingConfigReturnsError(t *testing.T) {
|
||||||
_, err := LoadFile(filepath.Join(t.TempDir(), "missing.yml"))
|
_, err := LoadFile(filepath.Join(t.TempDir(), "missing.yml"))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const hourlyHours = 6
|
const hourlyReportHours = 6
|
||||||
|
|
||||||
func hourlyDefinition() Definition {
|
func hourlyDefinition() Definition {
|
||||||
return Definition{
|
return Definition{
|
||||||
ID: Hourly,
|
ID: Hourly,
|
||||||
Name: "Hourly Report",
|
Name: "Hourly Report",
|
||||||
PromptID: "weather.hourly_report",
|
PromptID: "weather.hourly_generated_text",
|
||||||
GenerationMode: GenerationModeScriptoriumMarkdown,
|
GenerationMode: GenerationModeGeneratedTextTemplate,
|
||||||
|
TemplateID: "hourly",
|
||||||
|
GeneratedTextSchemaID: "hourly",
|
||||||
ComparisonStrategy: CompareRollingWindow,
|
ComparisonStrategy: CompareRollingWindow,
|
||||||
ArtifactGroup: "hourly",
|
ArtifactGroup: "hourly",
|
||||||
BatchOutputName: "hourly.md",
|
BatchOutputName: "hourly.md",
|
||||||
@@ -48,6 +50,6 @@ func resolveHourly(req ResolveRequest) (timeutil.Period, error) {
|
|||||||
localNow := req.Now.In(req.Location)
|
localNow := req.Now.In(req.Location)
|
||||||
return timeutil.Period{
|
return timeutil.Period{
|
||||||
Start: localNow,
|
Start: localNow,
|
||||||
End: localNow.Add(hourlyHours * time.Hour),
|
End: localNow.Add(hourlyReportHours * time.Hour),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,17 @@ func TestHourlyLookupAndPeriodCalculation(t *testing.T) {
|
|||||||
if resolved.Definition.ID != Hourly {
|
if resolved.Definition.ID != Hourly {
|
||||||
t.Fatalf("ID = %q, want hourly", resolved.Definition.ID)
|
t.Fatalf("ID = %q, want hourly", resolved.Definition.ID)
|
||||||
}
|
}
|
||||||
if resolved.Definition.PromptID != "weather.hourly_report" {
|
if resolved.Definition.PromptID != "weather.hourly_generated_text" {
|
||||||
t.Fatalf("PromptID = %q, want weather.hourly_report", resolved.Definition.PromptID)
|
t.Fatalf("PromptID = %q, want weather.hourly_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 != "hourly" {
|
||||||
|
t.Fatalf("TemplateID = %q, want hourly", resolved.Definition.TemplateID)
|
||||||
|
}
|
||||||
|
if resolved.Definition.GeneratedTextSchemaID != "hourly" {
|
||||||
|
t.Fatalf("GeneratedTextSchemaID = %q, want hourly", resolved.Definition.GeneratedTextSchemaID)
|
||||||
}
|
}
|
||||||
if resolved.Definition.ComparisonStrategy != CompareRollingWindow {
|
if resolved.Definition.ComparisonStrategy != CompareRollingWindow {
|
||||||
t.Fatalf("ComparisonStrategy = %q, want rolling_window", resolved.Definition.ComparisonStrategy)
|
t.Fatalf("ComparisonStrategy = %q, want rolling_window", resolved.Definition.ComparisonStrategy)
|
||||||
@@ -259,6 +268,18 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
|
|||||||
if !definition.Generated {
|
if !definition.Generated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if definition.ID == Hourly {
|
||||||
|
if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
|
||||||
|
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
|
||||||
|
}
|
||||||
|
if definition.TemplateID != "hourly" {
|
||||||
|
t.Fatalf("%s TemplateID = %q, want hourly", definition.ID, definition.TemplateID)
|
||||||
|
}
|
||||||
|
if definition.GeneratedTextSchemaID != "hourly" {
|
||||||
|
t.Fatalf("%s GeneratedTextSchemaID = %q, want hourly", definition.ID, definition.GeneratedTextSchemaID)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if definition.GenerationMode != GenerationModeScriptoriumMarkdown {
|
if definition.GenerationMode != GenerationModeScriptoriumMarkdown {
|
||||||
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeScriptoriumMarkdown)
|
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeScriptoriumMarkdown)
|
||||||
}
|
}
|
||||||
@@ -545,8 +566,8 @@ func TestHourlyMetadataRunIDIncludesReportID(t *testing.T) {
|
|||||||
if metadata.ReportID != Hourly {
|
if metadata.ReportID != Hourly {
|
||||||
t.Fatalf("ReportID = %q, want hourly", metadata.ReportID)
|
t.Fatalf("ReportID = %q, want hourly", metadata.ReportID)
|
||||||
}
|
}
|
||||||
if metadata.PromptID != "weather.hourly_report" {
|
if metadata.PromptID != "weather.hourly_generated_text" {
|
||||||
t.Fatalf("PromptID = %q, want weather.hourly_report", metadata.PromptID)
|
t.Fatalf("PromptID = %q, want weather.hourly_generated_text", metadata.PromptID)
|
||||||
}
|
}
|
||||||
if !strings.Contains(metadata.RunID, "hourly") {
|
if !strings.Contains(metadata.RunID, "hourly") {
|
||||||
t.Fatalf("RunID = %q, want report id", metadata.RunID)
|
t.Fatalf("RunID = %q, want report id", metadata.RunID)
|
||||||
|
|||||||
Reference in New Issue
Block a user