Add near-term report identity
This commit is contained in:
@@ -32,6 +32,9 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
|
|||||||
registry := MustDefaultModuleRegistry()
|
registry := MustDefaultModuleRegistry()
|
||||||
for _, definition := range report.DefaultRegistry().All() {
|
for _, definition := range report.DefaultRegistry().All() {
|
||||||
t.Run(string(definition.ID), func(t *testing.T) {
|
t.Run(string(definition.ID), func(t *testing.T) {
|
||||||
|
if len(definition.Modules) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx := derivedModuleContext(definition.ID)
|
ctx := derivedModuleContext(definition.ID)
|
||||||
var outputs []module.Output
|
var outputs []module.Output
|
||||||
for _, item := range definition.Modules {
|
for _, item := range definition.Modules {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type ID string
|
|||||||
const (
|
const (
|
||||||
DailyToday ID = "daily_today"
|
DailyToday ID = "daily_today"
|
||||||
DailyTomorrow ID = "daily_tomorrow"
|
DailyTomorrow ID = "daily_tomorrow"
|
||||||
|
NearTerm ID = "near_term"
|
||||||
ThreeDay ID = "three_day"
|
ThreeDay ID = "three_day"
|
||||||
Weekend ID = "weekend"
|
Weekend ID = "weekend"
|
||||||
Storm ID = "storm"
|
Storm ID = "storm"
|
||||||
@@ -25,6 +26,7 @@ const (
|
|||||||
CompareSameValidDate ComparisonStrategy = "same_valid_date"
|
CompareSameValidDate ComparisonStrategy = "same_valid_date"
|
||||||
CompareWeekendWindow ComparisonStrategy = "same_weekend_window"
|
CompareWeekendWindow ComparisonStrategy = "same_weekend_window"
|
||||||
CompareExplicitWindow ComparisonStrategy = "explicit_event_window"
|
CompareExplicitWindow ComparisonStrategy = "explicit_event_window"
|
||||||
|
CompareRollingWindow ComparisonStrategy = "rolling_window"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Batch string
|
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")
|
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) {
|
func TestWeekendPeriodCalculation(t *testing.T) {
|
||||||
location := mustLoadLocation(t)
|
location := mustLoadLocation(t)
|
||||||
tests := []struct {
|
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) {
|
func TestRegistryLookupErrorIsActionable(t *testing.T) {
|
||||||
_, err := DefaultRegistry().Lookup(ID("unknown"))
|
_, err := DefaultRegistry().Lookup(ID("unknown"))
|
||||||
if err == nil {
|
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) {
|
func TestRegistryDefinitionsHavePromptIDsAndComparisonStrategies(t *testing.T) {
|
||||||
for _, definition := range DefaultRegistry().All() {
|
for _, definition := range DefaultRegistry().All() {
|
||||||
if definition.PromptID == "" {
|
if definition.PromptID == "" {
|
||||||
@@ -186,6 +258,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName string
|
batchOutputName string
|
||||||
generated bool
|
generated bool
|
||||||
compatiblePriorIDs []ID
|
compatiblePriorIDs []ID
|
||||||
|
comparisonStrategy ComparisonStrategy
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
id: DailyToday,
|
id: DailyToday,
|
||||||
@@ -193,6 +266,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName: "daily.md",
|
batchOutputName: "daily.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
||||||
|
comparisonStrategy: CompareSameValidDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DailyTomorrow,
|
id: DailyTomorrow,
|
||||||
@@ -200,6 +274,15 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName: "tomorrow.md",
|
batchOutputName: "tomorrow.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
compatiblePriorIDs: []ID{DailyToday, DailyTomorrow},
|
||||||
|
comparisonStrategy: CompareSameValidDate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: NearTerm,
|
||||||
|
artifactGroup: "near-term",
|
||||||
|
batchOutputName: "near-term.md",
|
||||||
|
generated: true,
|
||||||
|
compatiblePriorIDs: []ID{NearTerm},
|
||||||
|
comparisonStrategy: CompareRollingWindow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: ThreeDay,
|
id: ThreeDay,
|
||||||
@@ -207,6 +290,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName: "three-day.md",
|
batchOutputName: "three-day.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{ThreeDay},
|
compatiblePriorIDs: []ID{ThreeDay},
|
||||||
|
comparisonStrategy: CompareSameValidDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: Weekend,
|
id: Weekend,
|
||||||
@@ -214,6 +298,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName: "weekend.md",
|
batchOutputName: "weekend.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{Weekend},
|
compatiblePriorIDs: []ID{Weekend},
|
||||||
|
comparisonStrategy: CompareWeekendWindow,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: Storm,
|
id: Storm,
|
||||||
@@ -221,6 +306,7 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
batchOutputName: "storm.md",
|
batchOutputName: "storm.md",
|
||||||
generated: true,
|
generated: true,
|
||||||
compatiblePriorIDs: []ID{Storm},
|
compatiblePriorIDs: []ID{Storm},
|
||||||
|
comparisonStrategy: CompareExplicitWindow,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,6 +326,9 @@ func TestRegistryDefinitionsDeclarePathAndCompatibilityPolicy(t *testing.T) {
|
|||||||
if definition.Generated != tt.generated {
|
if definition.Generated != tt.generated {
|
||||||
t.Fatalf("Generated = %t, want %t", 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) {
|
if !reflect.DeepEqual(definition.CompatiblePriorIDs, tt.compatiblePriorIDs) {
|
||||||
t.Fatalf("CompatiblePriorIDs = %#v, want %#v", 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,
|
module.HourlyForecast,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: NearTerm,
|
||||||
|
want: []module.ID{},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: ThreeDay,
|
id: ThreeDay,
|
||||||
want: []module.ID{
|
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) {
|
func assertPeriod(t *testing.T, period timeutil.Period, wantStart string, wantEnd string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if !period.IsValid() {
|
if !period.IsValid() {
|
||||||
@@ -433,6 +544,14 @@ func resolvedIDs(resolved []Resolved) []string {
|
|||||||
return ids
|
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 {
|
func mustLoadLocation(t *testing.T) *time.Location {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
location, err := time.LoadLocation("America/Chicago")
|
location, err := time.LoadLocation("America/Chicago")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func DefaultRegistry() Registry {
|
|||||||
definitions := []Definition{
|
definitions := []Definition{
|
||||||
dailyTodayDefinition(),
|
dailyTodayDefinition(),
|
||||||
dailyTomorrowDefinition(),
|
dailyTomorrowDefinition(),
|
||||||
|
nearTermDefinition(),
|
||||||
threeDayDefinition(),
|
threeDayDefinition(),
|
||||||
weekendDefinition(),
|
weekendDefinition(),
|
||||||
stormDefinition(),
|
stormDefinition(),
|
||||||
@@ -73,7 +74,7 @@ func (r Registry) MustLookup(id ID) Definition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r Registry) All() []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))
|
out := make([]Definition, 0, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if definition, ok := r.definitions[id]; ok {
|
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) {
|
func TestFilesystemStoreRejectsUnsafeDirs(t *testing.T) {
|
||||||
cfg := config.Defaults().Workspace
|
cfg := config.Defaults().Workspace
|
||||||
cfg.Root = t.TempDir()
|
cfg.Root = t.TempDir()
|
||||||
@@ -390,6 +405,26 @@ func resolveWeekendAt(t *testing.T, value string) report.Resolved {
|
|||||||
return 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 {
|
func stateBriefingMetadata(resolved report.Resolved) briefing.Metadata {
|
||||||
return briefing.Metadata{
|
return briefing.Metadata{
|
||||||
RunID: resolved.Metadata().RunID,
|
RunID: resolved.Metadata().RunID,
|
||||||
|
|||||||
Reference in New Issue
Block a user