Add near-term report identity

This commit is contained in:
2026-06-12 17:28:26 +00:00
parent 0b1423c90d
commit 5284033fb8
6 changed files with 198 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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

View 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
}

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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,