Add near-term report identity
This commit is contained in:
@@ -32,6 +32,9 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
|
||||
registry := MustDefaultModuleRegistry()
|
||||
for _, definition := range report.DefaultRegistry().All() {
|
||||
t.Run(string(definition.ID), func(t *testing.T) {
|
||||
if len(definition.Modules) == 0 {
|
||||
return
|
||||
}
|
||||
ctx := derivedModuleContext(definition.ID)
|
||||
var outputs []module.Output
|
||||
for _, item := range definition.Modules {
|
||||
|
||||
@@ -14,6 +14,7 @@ type ID string
|
||||
const (
|
||||
DailyToday ID = "daily_today"
|
||||
DailyTomorrow ID = "daily_tomorrow"
|
||||
NearTerm ID = "near_term"
|
||||
ThreeDay ID = "three_day"
|
||||
Weekend ID = "weekend"
|
||||
Storm ID = "storm"
|
||||
@@ -25,6 +26,7 @@ const (
|
||||
CompareSameValidDate ComparisonStrategy = "same_valid_date"
|
||||
CompareWeekendWindow ComparisonStrategy = "same_weekend_window"
|
||||
CompareExplicitWindow ComparisonStrategy = "explicit_event_window"
|
||||
CompareRollingWindow ComparisonStrategy = "rolling_window"
|
||||
)
|
||||
|
||||
type Batch string
|
||||
|
||||
37
internal/report/near_term_report.go
Normal file
37
internal/report/near_term_report.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package report
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
const nearTermHours = 6
|
||||
|
||||
func nearTermDefinition() Definition {
|
||||
return Definition{
|
||||
ID: NearTerm,
|
||||
Name: "Near-Term Report",
|
||||
PromptID: "weather.near_term_report",
|
||||
ComparisonStrategy: CompareRollingWindow,
|
||||
ArtifactGroup: "near-term",
|
||||
BatchOutputName: "near-term.md",
|
||||
Generated: true,
|
||||
CompatiblePriorIDs: []ID{NearTerm},
|
||||
Modules: nearTermModules(),
|
||||
resolve: resolveNearTerm,
|
||||
}
|
||||
}
|
||||
|
||||
func nearTermModules() []module.ConfigItem {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveNearTerm(req ResolveRequest) (timeutil.Period, error) {
|
||||
localNow := req.Now.In(req.Location)
|
||||
return timeutil.Period{
|
||||
Start: localNow,
|
||||
End: localNow.Add(nearTermHours * time.Hour),
|
||||
}, nil
|
||||
}
|
||||
@@ -58,6 +58,51 @@ func TestThreeDayPeriodCalculation(t *testing.T) {
|
||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-29T05:00:00-05:00", "2026-06-01T00:00:00-05:00")
|
||||
}
|
||||
|
||||
func TestNearTermLookupAndPeriodCalculation(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T05:15:00-05:00")
|
||||
|
||||
resolved, err := Resolve(NearTerm, ResolveRequest{Now: now, Location: location})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
if resolved.Definition.ID != NearTerm {
|
||||
t.Fatalf("ID = %q, want near_term", resolved.Definition.ID)
|
||||
}
|
||||
if resolved.Definition.PromptID != "weather.near_term_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.near_term_report", resolved.Definition.PromptID)
|
||||
}
|
||||
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 TestNearTermPeriodUsesEffectiveTimezone(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(NearTerm, 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 TestNearTermPeriodIsNotCivilDayTruncated(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
now := mustParse("2026-05-29T22:30:00-05:00")
|
||||
|
||||
resolved, err := Resolve(NearTerm, 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 {
|
||||
@@ -158,6 +203,25 @@ func TestEveningBatchIncludesTomorrow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchesDoNotIncludeNearTerm(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
req := ResolveRequest{Now: mustParse("2026-05-29T06:00:00-05:00"), Location: location}
|
||||
|
||||
morning, err := DefaultRegistry().BatchReports(Morning, req)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchReports(morning) error = %v", err)
|
||||
}
|
||||
evening, err := DefaultRegistry().BatchReports(Evening, req)
|
||||
if err != nil {
|
||||
t.Fatalf("BatchReports(evening) error = %v", err)
|
||||
}
|
||||
for _, resolved := range append(morning, evening...) {
|
||||
if resolved.Definition.ID == NearTerm {
|
||||
t.Fatalf("batch included %q, want near-term excluded", resolved.Definition.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryLookupErrorIsActionable(t *testing.T) {
|
||||
_, err := DefaultRegistry().Lookup(ID("unknown"))
|
||||
if err == nil {
|
||||
@@ -168,6 +232,14 @@ func TestRegistryLookupErrorIsActionable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryAllIncludesNearTermInStableOrder(t *testing.T) {
|
||||
ids := resolvedDefinitionIDs(DefaultRegistry().All())
|
||||
want := []string{"daily_today", "daily_tomorrow", "near_term", "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 == "" {
|
||||
@@ -186,6 +258,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName string
|
||||
generated bool
|
||||
compatiblePriorIDs []ID
|
||||
comparisonStrategy ComparisonStrategy
|
||||
}{
|
||||
{
|
||||
id: DailyToday,
|
||||
@@ -193,6 +266,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName: "daily.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: DailyTomorrow,
|
||||
@@ -200,6 +274,15 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName: "tomorrow.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: NearTerm,
|
||||
artifactGroup: "near-term",
|
||||
batchOutputName: "near-term.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{NearTerm},
|
||||
comparisonStrategy: CompareRollingWindow,
|
||||
},
|
||||
{
|
||||
id: ThreeDay,
|
||||
@@ -207,6 +290,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName: "three-day.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{ThreeDay},
|
||||
comparisonStrategy: CompareSameValidDate,
|
||||
},
|
||||
{
|
||||
id: Weekend,
|
||||
@@ -214,6 +298,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName: "weekend.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Weekend},
|
||||
comparisonStrategy: CompareWeekendWindow,
|
||||
},
|
||||
{
|
||||
id: Storm,
|
||||
@@ -221,6 +306,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
batchOutputName: "storm.md",
|
||||
generated: true,
|
||||
compatiblePriorIDs: []ID{Storm},
|
||||
comparisonStrategy: CompareExplicitWindow,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -240,6 +326,9 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -294,6 +383,10 @@ func TestRegistryDefinitionsDeclareDefaultModules(t *testing.T) {
|
||||
module.HourlyForecast,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: NearTerm,
|
||||
want: []module.ID{},
|
||||
},
|
||||
{
|
||||
id: ThreeDay,
|
||||
want: []module.ID{
|
||||
@@ -412,6 +505,24 @@ func TestResolvedMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearTermMetadataRunIDIncludesReportID(t *testing.T) {
|
||||
location := mustLoadLocation(t)
|
||||
resolved, err := Resolve(NearTerm, 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 != NearTerm {
|
||||
t.Fatalf("ReportID = %q, want near_term", metadata.ReportID)
|
||||
}
|
||||
if metadata.PromptID != "weather.near_term_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.near_term_report", metadata.PromptID)
|
||||
}
|
||||
if !strings.Contains(metadata.RunID, "near_term") {
|
||||
t.Fatalf("RunID = %q, want report id", metadata.RunID)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) {
|
||||
t.Helper()
|
||||
if !period.IsValid() {
|
||||
@@ -433,6 +544,14 @@ func resolvedIDs(resolved []Resolved) []string {
|
||||
return ids
|
||||
}
|
||||
|
||||
func resolvedDefinitionIDs(definitions []Definition) []string {
|
||||
ids := make([]string, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
ids = append(ids, string(definition.ID))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func mustLoadLocation(t *testing.T) *time.Location {
|
||||
t.Helper()
|
||||
location, err := time.LoadLocation("America/Chicago")
|
||||
|
||||
@@ -14,6 +14,7 @@ func DefaultRegistry() Registry {
|
||||
definitions := []Definition{
|
||||
dailyTodayDefinition(),
|
||||
dailyTomorrowDefinition(),
|
||||
nearTermDefinition(),
|
||||
threeDayDefinition(),
|
||||
weekendDefinition(),
|
||||
stormDefinition(),
|
||||
@@ -73,7 +74,7 @@ func (r Registry) MustLookup(id ID) Definition {
|
||||
}
|
||||
|
||||
func (r Registry) All() []Definition {
|
||||
ids := []ID{DailyToday, DailyTomorrow, ThreeDay, Weekend, Storm}
|
||||
ids := []ID{DailyToday, DailyTomorrow, NearTerm, ThreeDay, Weekend, Storm}
|
||||
out := make([]Definition, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if definition, ok := r.definitions[id]; ok {
|
||||
|
||||
@@ -305,6 +305,21 @@ func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPriorSnapshotIgnoresRollingWindowReports(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
first := resolveNearTermAt(t, "2026-05-29T05:00:00-05:00")
|
||||
second := resolveNearTermAt(t, "2026-05-29T06:00:00-05:00")
|
||||
savePriorMetadata(t, store, first, stateBriefingMetadata(first))
|
||||
|
||||
prior, err := store.FindPriorSnapshot(context.Background(), second)
|
||||
if err != nil {
|
||||
t.Fatalf("FindPriorSnapshot() error = %v", err)
|
||||
}
|
||||
if prior != nil {
|
||||
t.Fatalf("FindPriorSnapshot() = %#v, want nil for rolling-window comparison", prior)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemStoreRejectsUnsafeDirs(t *testing.T) {
|
||||
cfg := config.Defaults().Workspace
|
||||
cfg.Root = t.TempDir()
|
||||
@@ -390,6 +405,26 @@ func resolveWeekendAt(t *testing.T, value string) report.Resolved {
|
||||
return resolved
|
||||
}
|
||||
|
||||
func resolveNearTermAt(t *testing.T, value string) report.Resolved {
|
||||
t.Helper()
|
||||
location, err := timeutil.LoadLocation("America/Chicago")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadLocation() error = %v", err)
|
||||
}
|
||||
now, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
t.Fatalf("parse time: %v", err)
|
||||
}
|
||||
resolved, err := report.DefaultRegistry().Resolve(report.NearTerm, report.ResolveRequest{
|
||||
Now: now,
|
||||
Location: location,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
func stateBriefingMetadata(resolved report.Resolved) briefing.Metadata {
|
||||
return briefing.Metadata{
|
||||
RunID: resolved.Metadata().RunID,
|
||||
|
||||
Reference in New Issue
Block a user