Prepare reports for Promptkit migration
This commit is contained in:
@@ -1,890 +1,135 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
func TestDailyValidPeriod(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
func TestResolveRetainedReportPeriods(t *testing.T) {
|
||||
location := loadTestLocation(t)
|
||||
now := parseTestTime("2026-05-29T17:45:00-05:00")
|
||||
date := parseTestTime("2026-05-31T12:00:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Daily, ResolveRequest{Now: now, Location: location})
|
||||
if err == nil {
|
||||
t.Fatal("Resolve() error = nil, want explicit date requirement")
|
||||
}
|
||||
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) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
date := mustParse("2026-05-31T12:00:00-05:00")
|
||||
|
||||
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) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Today, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T00:00:00-05:00", "2026-05-30T00:00:00-05:00")
|
||||
if resolved.Definition.PromptID != "weather.today_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.today_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 != "today" {
|
||||
t.Fatalf("TemplateID = %q, want today", resolved.Definition.TemplateID)
|
||||
}
|
||||
if resolved.Definition.GeneratedTextSchemaID != "today" {
|
||||
t.Fatalf("GeneratedTextSchemaID = %q, want today", resolved.Definition.GeneratedTextSchemaID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTodayValidPeriodCanUseExplicitDate(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T17:45:00-05:00")
|
||||
date := mustParse("2026-05-31T12:00:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Today, 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")
|
||||
}
|
||||
|
||||
func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T20:00:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Tomorrow, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00")
|
||||
if resolved.Definition.PromptID != "weather.tomorrow_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.tomorrow_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 != "tomorrow" {
|
||||
t.Fatalf("TemplateID = %q, want tomorrow", resolved.Definition.TemplateID)
|
||||
}
|
||||
if resolved.Definition.GeneratedTextSchemaID != "tomorrow" {
|
||||
t.Fatalf("GeneratedTextSchemaID = %q, want tomorrow", resolved.Definition.GeneratedTextSchemaID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThreeDayPeriodCalculation(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T05:00:00-05:00")
|
||||
|
||||
resolved, err := Resolve(ThreeDay, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:00:00-05:00", "2026-06-01T00:00:00-05:00")
|
||||
}
|
||||
|
||||
func TestHourlyLookupAndPeriodCalculation(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T05:15:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
if resolved.Definition.ID != Hourly {
|
||||
t.Fatalf("ID = %q, want hourly", resolved.Definition.ID)
|
||||
}
|
||||
if resolved.Definition.PromptID != "weather.hourly_generated_text" {
|
||||
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 {
|
||||
t.Fatalf("ComparisonStrategy = %q, want rolling_window", resolved.Definition.ComparisonStrategy)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:15:00-05:00", "2026-05-29T11:15:00-05:00")
|
||||
}
|
||||
|
||||
func TestHourlyPeriodUsesEffectiveTimezone(t *testing.T) {
|
||||
location, err := time.LoadLocation("America/New_York")
|
||||
if err != nil {
|
||||
t.Fatalf("load location: %v", err)
|
||||
}
|
||||
now := mustParse("2026-05-29T10:15:00Z")
|
||||
|
||||
resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T06:15:00-04:00", "2026-05-29T12:15:00-04:00")
|
||||
}
|
||||
|
||||
func TestHourlyPeriodIsNotCivilDayTruncated(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T22:30:00-05:00")
|
||||
|
||||
resolved, err := Resolve(Hourly, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T22:30:00-05:00", "2026-05-30T04:30:00-05:00")
|
||||
}
|
||||
|
||||
func TestWeekendPeriodCalculation(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
now string
|
||||
start string
|
||||
end string
|
||||
id ID
|
||||
request ResolveRequest
|
||||
wantStart string
|
||||
wantEnd string
|
||||
}{
|
||||
{name: "monday", now: "2026-05-25T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "tuesday", now: "2026-05-26T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "wednesday", now: "2026-05-27T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "thursday", now: "2026-05-28T05:00:00-05:00", start: "2026-05-30T00:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "friday before evening", now: "2026-05-29T05:00:00-05:00", start: "2026-05-29T18:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "friday after evening", now: "2026-05-29T19:30:00-05:00", start: "2026-05-29T19:30:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{name: "saturday", now: "2026-05-30T08:00:00-05:00", start: "2026-05-30T08:00:00-05:00", end: "2026-06-01T00:00:00-05:00"},
|
||||
{Daily, ResolveRequest{Now: now, Location: location, Date: date}, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00"},
|
||||
{Today, ResolveRequest{Now: now, Location: location, Date: date}, "2026-05-31T00:00:00-05:00", "2026-06-01T00:00:00-05:00"},
|
||||
{Tomorrow, ResolveRequest{Now: now, Location: location}, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00"},
|
||||
{Hourly, ResolveRequest{Now: now, Location: location}, "2026-05-29T17:45:00-05:00", "2026-05-29T23:45:00-05:00"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resolved, err := Resolve(Weekend, ResolveRequest{Now: mustParse(tt.now), Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, tt.start, tt.end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeekendSundayErrors(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
_, err := Resolve(Weekend, ResolveRequest{Now: mustParse("2026-05-31T08:00:00-05:00"), Location: location})
|
||||
if err == nil {
|
||||
t.Fatal("Resolve() error = nil, want Sunday weekend error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Sunday") {
|
||||
t.Fatalf("error = %q, want Sunday context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStormManualPeriodParsingAndValidation(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
period, err := ParseStormPeriod("2026-05-29T18:00", "2026-05-30T06:00:00-05:00", location)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseStormPeriod() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, period, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00")
|
||||
|
||||
_, err = ParseStormPeriod("2026-05-30T06:00", "2026-05-29T18:00", location)
|
||||
if err == nil {
|
||||
t.Fatal("ParseStormPeriod() error = nil, want invalid period error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStormResolve(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
resolved, err := Resolve(Storm, ResolveRequest{
|
||||
Now: mustParse("2026-05-29T12:00:00-05:00"),
|
||||
Location: location,
|
||||
StormStart: mustParse("2026-05-29T18:00:00-05:00"),
|
||||
StormEnd: mustParse("2026-05-30T06:00:00-05:00"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T18:00:00-05:00", "2026-05-30T06:00:00-05:00")
|
||||
if resolved.Definition.ComparisonStrategy != CompareExplicitWindow {
|
||||
t.Fatalf("ComparisonStrategy = %q, want explicit event window", resolved.Definition.ComparisonStrategy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDForCommandName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want ID
|
||||
}{
|
||||
{name: "daily", want: Daily},
|
||||
{name: "today", want: Today},
|
||||
{name: "tomorrow", want: Tomorrow},
|
||||
{name: "hourly", want: Hourly},
|
||||
{name: "three-day", want: ThreeDay},
|
||||
{name: "weekend", want: Weekend},
|
||||
{name: "storm", want: Storm},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := IDForCommandName(tt.name)
|
||||
if err != nil {
|
||||
t.Fatalf("IDForCommandName() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("IDForCommandName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if names := strings.Join(CommandNames(), ","); names != "daily,today,tomorrow,hourly,three-day,weekend,storm" {
|
||||
t.Fatalf("CommandNames() = %s, want stable command names", names)
|
||||
}
|
||||
dailyID, err := IDForCommandName("daily")
|
||||
if err != nil {
|
||||
t.Fatalf("IDForCommandName(daily) error = %v", err)
|
||||
}
|
||||
todayID, err := IDForCommandName("today")
|
||||
if err != nil {
|
||||
t.Fatalf("IDForCommandName(today) error = %v", err)
|
||||
}
|
||||
if dailyID == todayID {
|
||||
t.Fatalf("daily and today both resolve to %q, want distinct report IDs", dailyID)
|
||||
}
|
||||
if _, err := IDForCommandName("near-term"); err == nil || !strings.Contains(err.Error(), `unknown report command "near-term"`) {
|
||||
t.Fatalf("IDForCommandName(near-term) error = %v, want unknown command", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIDForConfigKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
want ID
|
||||
}{
|
||||
{key: "daily", want: Daily},
|
||||
{key: "today", want: Today},
|
||||
{key: "tomorrow", want: Tomorrow},
|
||||
{key: "hourly", want: Hourly},
|
||||
{key: "three_day", want: ThreeDay},
|
||||
{key: "three-day", want: ThreeDay},
|
||||
{key: "three_day_outlook", want: ThreeDay},
|
||||
{key: "three-day-outlook", want: ThreeDay},
|
||||
{key: "weekend", want: Weekend},
|
||||
{key: "weekend_outlook", want: Weekend},
|
||||
{key: "storm", want: Storm},
|
||||
{key: "storm_report", want: Storm},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
got, err := IDForConfigKey(tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("IDForConfigKey() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("IDForConfigKey() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
}
|
||||
retiredDailyKey := strings.Join([]string{"daily", "today"}, "_")
|
||||
if _, err := IDForConfigKey(retiredDailyKey); err == nil || !strings.Contains(err.Error(), `report config key "`+retiredDailyKey+`" is not a known report`) {
|
||||
t.Fatalf("IDForConfigKey(%s) error = %v, want unknown key", retiredDailyKey, err)
|
||||
}
|
||||
dailyID, err := IDForConfigKey("daily")
|
||||
if err != nil {
|
||||
t.Fatalf("IDForConfigKey(daily) error = %v", err)
|
||||
}
|
||||
todayID, err := IDForConfigKey("today")
|
||||
if err != nil {
|
||||
t.Fatalf("IDForConfigKey(today) error = %v", err)
|
||||
}
|
||||
if dailyID == todayID {
|
||||
t.Fatalf("daily and today config keys both resolve to %q, want distinct report IDs", dailyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchForCommandName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want Batch
|
||||
}{
|
||||
{name: "morning", want: Morning},
|
||||
{name: "evening", want: Evening},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := BatchForCommandName(tt.name)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchForCommandName() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("BatchForCommandName() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if names := strings.Join(BatchCommandNames(), ","); names != "morning,evening" {
|
||||
t.Fatalf("BatchCommandNames() = %s, want stable batch command names", names)
|
||||
}
|
||||
if _, err := BatchForCommandName("hourly"); err == nil || !strings.Contains(err.Error(), `unknown batch command "hourly"`) {
|
||||
t.Fatalf("BatchForCommandName(hourly) error = %v, want unknown batch", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryLookupErrorIsActionable(t *testing.T) {
|
||||
_, err := DefaultRegistry().Lookup(ID("unknown"))
|
||||
if err == nil {
|
||||
t.Fatal("Lookup() error = nil, want unknown report error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), `unknown report "unknown"`) {
|
||||
t.Fatalf("error = %q, want unknown report context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryAllIncludesHourlyInStableOrder(t *testing.T) {
|
||||
ids := resolvedDefinitionIDs(DefaultRegistry().All())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsHavePromptIDsAndComparisonStrategies(t *testing.T) {
|
||||
for _, definition := range DefaultRegistry().All() {
|
||||
if definition.PromptID == "" {
|
||||
t.Fatalf("%s PromptID is empty", definition.ID)
|
||||
}
|
||||
if definition.Generated && definition.GenerationMode == "" {
|
||||
t.Fatalf("%s GenerationMode is empty", definition.ID)
|
||||
}
|
||||
if definition.ComparisonStrategy == "" {
|
||||
t.Fatalf("%s ComparisonStrategy is empty", definition.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
|
||||
for _, definition := range DefaultRegistry().All() {
|
||||
if !definition.Generated {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
if definition.TemplateID != wantTemplate {
|
||||
t.Fatalf("%s TemplateID = %q, want %s", definition.ID, definition.TemplateID, wantTemplate)
|
||||
}
|
||||
if definition.GeneratedTextSchemaID != wantTemplate {
|
||||
t.Fatalf("%s GeneratedTextSchemaID = %q, want %s", definition.ID, definition.GeneratedTextSchemaID, wantTemplate)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if definition.GenerationMode != GenerationModeScriptoriumMarkdown {
|
||||
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeScriptoriumMarkdown)
|
||||
}
|
||||
if definition.TemplateID != "" {
|
||||
t.Fatalf("%s TemplateID = %q, want empty for Markdown report", definition.ID, definition.TemplateID)
|
||||
}
|
||||
if definition.GeneratedTextSchemaID != "" {
|
||||
t.Fatalf("%s GeneratedTextSchemaID = %q, want empty for Markdown report", definition.ID, definition.GeneratedTextSchemaID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
id ID
|
||||
artifactGroup string
|
||||
batchOutputName string
|
||||
generated bool
|
||||
compatiblePriorIDs []ID
|
||||
comparisonStrategy ComparisonStrategy
|
||||
}{
|
||||
{
|
||||
id: Daily,
|
||||
artifactGroup: "daily",
|
||||
batchOutputName: "daily.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Daily},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: Today,
|
||||
artifactGroup: "today",
|
||||
batchOutputName: "today.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Today},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: Tomorrow,
|
||||
artifactGroup: "tomorrow",
|
||||
batchOutputName: "tomorrow.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Tomorrow},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: Hourly,
|
||||
artifactGroup: "hourly",
|
||||
batchOutputName: "hourly.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Hourly},
|
||||
comparisonStrategy: CompareRollingWindow,
|
||||
},
|
||||
{
|
||||
id: ThreeDay,
|
||||
artifactGroup: "three-day",
|
||||
batchOutputName: "three-day.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{ThreeDay},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: Weekend,
|
||||
artifactGroup: "weekend",
|
||||
batchOutputName: "weekend.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Weekend},
|
||||
comparisonStrategy: CompareWeekendWindow,
|
||||
},
|
||||
{
|
||||
id: Storm,
|
||||
artifactGroup: "storm",
|
||||
batchOutputName: "storm.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Storm},
|
||||
comparisonStrategy: CompareExplicitWindow,
|
||||
},
|
||||
}
|
||||
|
||||
registry := DefaultRegistry()
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.id), func(t *testing.T) {
|
||||
definition, err := registry.Lookup(tt.id)
|
||||
resolved, err := Resolve(tt.id, tt.request)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
if definition.ArtifactGroup != tt.artifactGroup {
|
||||
t.Fatalf("ArtifactGroup = %q, want %q", definition.ArtifactGroup, tt.artifactGroup)
|
||||
}
|
||||
if definition.BatchOutputName != tt.batchOutputName {
|
||||
t.Fatalf("BatchOutputName = %q, want %q", definition.BatchOutputName, tt.batchOutputName)
|
||||
}
|
||||
if definition.Generated != tt.generated {
|
||||
t.Fatalf("Generated = %t, want %t", definition.Generated, tt.generated)
|
||||
}
|
||||
if definition.ComparisonStrategy != tt.comparisonStrategy {
|
||||
t.Fatalf("ComparisonStrategy = %q, want %q", definition.ComparisonStrategy, tt.comparisonStrategy)
|
||||
}
|
||||
if !reflect.DeepEqual(definition.CompatiblePriorIDs, tt.compatiblePriorIDs) {
|
||||
t.Fatalf("CompatiblePriorIDs = %#v, want %#v", definition.CompatiblePriorIDs, tt.compatiblePriorIDs)
|
||||
}
|
||||
for _, id := range tt.compatiblePriorIDs {
|
||||
if !definition.CompatibleWithPrior(id) {
|
||||
t.Fatalf("CompatibleWithPrior(%q) = false, want true", id)
|
||||
}
|
||||
t.Fatalf("Resolve(%q) error = %v", tt.id, err)
|
||||
}
|
||||
assertTestPeriod(t, resolved.ValidPeriod, tt.wantStart, tt.wantEnd)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedRegistryDefinitionsDeclareDistributorPathDefaults(t *testing.T) {
|
||||
for _, definition := range DefaultRegistry().All() {
|
||||
if !definition.Generated {
|
||||
continue
|
||||
}
|
||||
if len(definition.DistributorPathTemplates) == 0 {
|
||||
t.Fatalf("%s DistributorPathTemplates is empty", definition.ID)
|
||||
}
|
||||
func TestResolveDailyRequiresDate(t *testing.T) {
|
||||
_, err := Resolve(Daily, ResolveRequest{Now: parseTestTime("2026-05-29T17:45:00-05:00"), Location: loadTestLocation(t)})
|
||||
if err == nil || !strings.Contains(err.Error(), "explicit date") {
|
||||
t.Fatalf("Resolve(Daily) error = %v, want explicit-date error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsDeclareDefaultDistributorPathTemplates(t *testing.T) {
|
||||
tests := []struct {
|
||||
id ID
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
id: Hourly,
|
||||
want: []string{
|
||||
"hourly/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Daily,
|
||||
want: []string{
|
||||
"daily/{valid_start_date}/{run_id}.md",
|
||||
"daily/{valid_start_date}/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Today,
|
||||
want: []string{
|
||||
"daily/{valid_start_date}/{run_id}.md",
|
||||
"daily/{valid_start_date}/index.md",
|
||||
"today/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Tomorrow,
|
||||
want: []string{
|
||||
"daily/{valid_start_date}/{run_id}.md",
|
||||
"daily/{valid_start_date}/index.md",
|
||||
"tomorrow/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ThreeDay,
|
||||
want: []string{
|
||||
"three-day/{valid_start_date}/{run_id}.md",
|
||||
"three-day/{valid_start_date}/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Weekend,
|
||||
want: []string{
|
||||
"weekend/{valid_start_date}/{run_id}.md",
|
||||
"weekend/{valid_start_date}/index.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Storm,
|
||||
want: []string{
|
||||
"storm/{storm_id}/{run_id}.md",
|
||||
"storm/{storm_id}/index.md",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestRegistryContainsOnlyPromptBackedReports(t *testing.T) {
|
||||
registry := DefaultRegistry()
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.id), func(t *testing.T) {
|
||||
definition, err := registry.Lookup(tt.id)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(definition.DistributorPathTemplates, tt.want) {
|
||||
t.Fatalf("DistributorPathTemplates = %#v, want %#v", definition.DistributorPathTemplates, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
tests := []struct {
|
||||
id ID
|
||||
want []module.ID
|
||||
}{
|
||||
{
|
||||
id: Daily,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.NarrativeForecast,
|
||||
module.DerivedDailySummary,
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.DailyPlanning,
|
||||
module.HourlyForecast,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Today,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.NarrativeForecast,
|
||||
module.DerivedDailySummary,
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.HourlyForecast,
|
||||
module.TodayPlanning,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Tomorrow,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.NarrativeForecast,
|
||||
module.DerivedDailySummary,
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
module.TomorrowPlanning,
|
||||
module.HourlyForecast,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Hourly,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.HourlyForecast,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ThreeDay,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Weekend,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.DerivedDaypartSummaries,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
module.OutdoorWindows,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: Storm,
|
||||
want: []module.ID{
|
||||
module.Metadata,
|
||||
module.CurrentConditions,
|
||||
module.PrecipTiming,
|
||||
module.AlertDigest,
|
||||
module.SPCConvectiveOutlooks,
|
||||
module.AreaForecastDiscussion,
|
||||
module.SPCConvectiveDiscussion,
|
||||
module.WeatherStory,
|
||||
},
|
||||
},
|
||||
definitions := registry.All()
|
||||
if len(definitions) != 4 {
|
||||
t.Fatalf("Registry.All() length = %d, want 4", len(definitions))
|
||||
}
|
||||
|
||||
registry := DefaultRegistry()
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.id), func(t *testing.T) {
|
||||
definition, err := registry.Lookup(tt.id)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(definition.ModuleIDs(), tt.want) {
|
||||
t.Fatalf("ModuleIDs() = %#v, want %#v", definition.ModuleIDs(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryAppliesModuleOverridesWithoutChangingDefaults(t *testing.T) {
|
||||
base := DefaultRegistry()
|
||||
overridden, err := base.WithModuleOverrides(map[ID][]module.ConfigItem{
|
||||
Daily: {
|
||||
{ID: module.Metadata},
|
||||
{ID: module.AlertDigest},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("WithModuleOverrides() error = %v", err)
|
||||
}
|
||||
|
||||
definition, err := overridden.Lookup(Daily)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(overridden) error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(definition.ModuleIDs(), []module.ID{module.Metadata, module.AlertDigest}) {
|
||||
t.Fatalf("overridden ModuleIDs() = %#v, want metadata and alert digest", definition.ModuleIDs())
|
||||
}
|
||||
|
||||
defaultDefinition, err := base.Lookup(Daily)
|
||||
if err != nil {
|
||||
t.Fatalf("Lookup(default) error = %v", err)
|
||||
}
|
||||
if len(defaultDefinition.ModuleIDs()) <= len(definition.ModuleIDs()) {
|
||||
t.Fatalf("default ModuleIDs() = %#v, want original defaults unchanged", defaultDefinition.ModuleIDs())
|
||||
}
|
||||
if !reflect.DeepEqual(definition.DistributorPathTemplates, defaultDefinition.DistributorPathTemplates) {
|
||||
t.Fatalf("overridden DistributorPathTemplates = %#v, want %#v", definition.DistributorPathTemplates, defaultDefinition.DistributorPathTemplates)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryRejectsModuleOverrideForUnknownReport(t *testing.T) {
|
||||
_, err := DefaultRegistry().WithModuleOverrides(map[ID][]module.ConfigItem{
|
||||
ID("unknown"): {{ID: module.Metadata}},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("WithModuleOverrides() error = nil, want unknown report")
|
||||
}
|
||||
if !strings.Contains(err.Error(), `unknown report "unknown"`) {
|
||||
t.Fatalf("error = %q, want unknown report context", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedMetadata(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
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 != Daily {
|
||||
t.Fatalf("ReportID = %q, want daily", metadata.ReportID)
|
||||
}
|
||||
if metadata.PromptID != "weather.daily_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_generated_text", metadata.PromptID)
|
||||
}
|
||||
if metadata.RunID != "20260529T100000.000000000Z_daily_2026-05-29" {
|
||||
t.Fatalf("RunID = %q, want dated Daily report id", metadata.RunID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDailyRunIDIncludesValidDateDisambiguator(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T05:00:00-05:00")
|
||||
first, err := Resolve(Daily, ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
Date: mustParse("2026-05-31T12:00:00-05:00"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(first) error = %v", err)
|
||||
}
|
||||
second, err := Resolve(Daily, ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
Date: mustParse("2026-06-01T12:00:00-05:00"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve(second) error = %v", err)
|
||||
}
|
||||
|
||||
firstRunID := first.Metadata().RunID
|
||||
secondRunID := second.Metadata().RunID
|
||||
if firstRunID == secondRunID {
|
||||
t.Fatalf("Daily RunIDs both = %q, want distinct valid-date suffixes", firstRunID)
|
||||
}
|
||||
if firstRunID != "20260529T100000.000000000Z_daily_2026-05-31" {
|
||||
t.Fatalf("first RunID = %q, want valid-date suffix", firstRunID)
|
||||
}
|
||||
if secondRunID != "20260529T100000.000000000Z_daily_2026-06-01" {
|
||||
t.Fatalf("second RunID = %q, want valid-date suffix", secondRunID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHourlyMetadataRunIDIncludesReportID(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
resolved, err := Resolve(Hourly, ResolveRequest{Now: mustParse("2026-05-29T05:15:00-05:00"), Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
metadata := resolved.Metadata()
|
||||
if metadata.ReportID != Hourly {
|
||||
t.Fatalf("ReportID = %q, want hourly", metadata.ReportID)
|
||||
}
|
||||
if metadata.PromptID != "weather.hourly_generated_text" {
|
||||
t.Fatalf("PromptID = %q, want weather.hourly_generated_text", metadata.PromptID)
|
||||
}
|
||||
if metadata.RunID != "20260529T101500.000000000Z_hourly" {
|
||||
t.Fatalf("RunID = %q, want unchanged report id shape", metadata.RunID)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) {
|
||||
t.Helper()
|
||||
if !period.IsValid() {
|
||||
t.Fatalf("period = %#v, want valid", period)
|
||||
}
|
||||
if period.Start.Format(time.RFC3339) != wantStart {
|
||||
t.Fatalf("Start = %s, want %s", period.Start.Format(time.RFC3339), wantStart)
|
||||
}
|
||||
if period.End.Format(time.RFC3339) != wantEnd {
|
||||
t.Fatalf("End = %s, want %s", period.End.Format(time.RFC3339), wantEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func resolvedDefinitionIDs(definitions []Definition) []string {
|
||||
ids := make([]string, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
ids = append(ids, string(definition.ID))
|
||||
if definition.PromptVersion != "1.0.0" {
|
||||
t.Fatalf("%s PromptVersion = %q, want 1.0.0", definition.ID, definition.PromptVersion)
|
||||
}
|
||||
if definition.TemplateID == "" || definition.GeneratedTextSchemaID == "" {
|
||||
t.Fatalf("%s template/schema = %q/%q, want both set", definition.ID, definition.TemplateID, definition.GeneratedTextSchemaID)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := registry.Lookup(ID("three_day")); err == nil {
|
||||
t.Fatal("Lookup(three_day) error = nil, want unknown report")
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func mustLoadLocation(t *testing.T) *time.Location {
|
||||
func TestCommandAndConfigurationNamesRejectRetiredReports(t *testing.T) {
|
||||
if names := strings.Join(CommandNames(), ","); names != "daily,today,tomorrow,hourly" {
|
||||
t.Fatalf("CommandNames() = %q", names)
|
||||
}
|
||||
|
||||
for _, name := range []string{"three-day", "weekend", "storm"} {
|
||||
if _, err := IDForCommandName(name); err == nil {
|
||||
t.Fatalf("IDForCommandName(%q) error = nil, want unknown command", name)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"three_day", "three_day_outlook", "weekend", "weekend_outlook", "storm", "storm_report"} {
|
||||
if _, err := IDForConfigKey(key); err == nil {
|
||||
t.Fatalf("IDForConfigKey(%q) error = nil, want unknown report", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryDefinitionsPreserveRetainedContracts(t *testing.T) {
|
||||
tests := []struct {
|
||||
id ID
|
||||
comparison ComparisonStrategy
|
||||
morning bool
|
||||
evening bool
|
||||
outputName string
|
||||
paths []string
|
||||
}{
|
||||
{Daily, CompareSameValidDate, false, false, "daily.md", []string{"daily/{valid_start_date}/{run_id}.md", "daily/{valid_start_date}/index.md"}},
|
||||
{Today, CompareSameValidDate, true, false, "today.md", []string{"daily/{valid_start_date}/{run_id}.md", "daily/{valid_start_date}/index.md", "today/index.md"}},
|
||||
{Tomorrow, CompareSameValidDate, false, true, "tomorrow.md", []string{"daily/{valid_start_date}/{run_id}.md", "daily/{valid_start_date}/index.md", "tomorrow/index.md"}},
|
||||
{Hourly, CompareRollingWindow, false, false, "hourly.md", []string{"hourly/index.md"}},
|
||||
}
|
||||
|
||||
registry := DefaultRegistry()
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.id), func(t *testing.T) {
|
||||
definition := registry.MustLookup(tt.id)
|
||||
if definition.ComparisonStrategy != tt.comparison || definition.Morning != tt.morning || definition.Evening != tt.evening || definition.BatchOutputName != tt.outputName {
|
||||
t.Fatalf("definition = %#v, want retained report contract", definition)
|
||||
}
|
||||
if strings.Join(definition.DistributorPathTemplates, "|") != strings.Join(tt.paths, "|") {
|
||||
t.Fatalf("DistributorPathTemplates = %#v, want %#v", definition.DistributorPathTemplates, tt.paths)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertTestPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) {
|
||||
t.Helper()
|
||||
location, err := time.LoadLocation("America/Chicago")
|
||||
if got := period.Start.Format(time.RFC3339); got != wantStart {
|
||||
t.Fatalf("period start = %q, want %q", got, wantStart)
|
||||
}
|
||||
if got := period.End.Format(time.RFC3339); got != wantEnd {
|
||||
t.Fatalf("period end = %q, want %q", got, wantEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func loadTestLocation(t *testing.T) *time.Location {
|
||||
t.Helper()
|
||||
location, err := timeutil.LoadLocation("America/Chicago")
|
||||
if err != nil {
|
||||
t.Fatalf("load location: %v", err)
|
||||
t.Fatalf("LoadLocation() error = %v", err)
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
func mustParse(value string) time.Time {
|
||||
func parseTestTime(value string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
Reference in New Issue
Block a user