Write prompt data packages as YAML
This commit is contained in:
@@ -1,72 +1,93 @@
|
||||
// Package promptinput builds prompt data packages from briefing packages.
|
||||
// Package promptinput builds prompt data packages from module snapshots.
|
||||
package promptinput
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/changes"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const SchemaVersion = "weatherreporter.data_package.v1"
|
||||
const SchemaVersion = "weatherreporter.data_package.v2"
|
||||
|
||||
type BuildRequest struct {
|
||||
Metadata Metadata
|
||||
Modules module.Snapshot
|
||||
RecentChanges []changes.Change
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
RunID string
|
||||
ReportID report.ID
|
||||
Variant string
|
||||
PromptID string
|
||||
GeneratedAt time.Time
|
||||
Timezone string
|
||||
ValidPeriod timeutil.Period
|
||||
SourceWarnings []weatherdata.SourceWarning
|
||||
}
|
||||
|
||||
type Package struct {
|
||||
SchemaVersion string `json:"schemaVersion"`
|
||||
RunID string `json:"runId"`
|
||||
Report Report `json:"report"`
|
||||
Briefing briefing.Package `json:"briefing"`
|
||||
RecentChanges RecentChanges `json:"recentChanges"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion" yaml:"schema_version"`
|
||||
RunID string `json:"runId" yaml:"run_id"`
|
||||
Report Report `json:"report" yaml:"report"`
|
||||
Briefing BriefingStanzas `json:"briefing" yaml:"briefing"`
|
||||
RecentChanges RecentChanges `json:"recentChanges" yaml:"recent_changes"`
|
||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty" yaml:"source_warnings,omitempty"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID report.ID `json:"id"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
PromptID string `json:"promptId"`
|
||||
GeneratedAt time.Time `json:"generatedAt"`
|
||||
Timezone string `json:"timezone"`
|
||||
CurrentLocalDate string `json:"currentLocalDate"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||
ID report.ID `json:"id" yaml:"id"`
|
||||
Variant string `json:"variant,omitempty" yaml:"variant,omitempty"`
|
||||
PromptID string `json:"promptId" yaml:"prompt_id"`
|
||||
GeneratedAt time.Time `json:"generatedAt" yaml:"generated_at"`
|
||||
Timezone string `json:"timezone" yaml:"timezone"`
|
||||
CurrentLocalDate string `json:"currentLocalDate" yaml:"current_local_date"`
|
||||
ValidPeriod timeutil.Period `json:"validPeriod" yaml:"valid_period"`
|
||||
}
|
||||
|
||||
type BriefingStanzas struct {
|
||||
Order []string `json:"-" yaml:"-"`
|
||||
Values map[string]any `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
type RecentChanges struct {
|
||||
Items []changes.Change `json:"items"`
|
||||
Items []changes.Change `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func Build(briefingPackage briefing.Package) (Package, error) {
|
||||
return BuildWithRecentChanges(briefingPackage, nil)
|
||||
}
|
||||
|
||||
func BuildWithRecentChanges(briefingPackage briefing.Package, recentChanges []changes.Change) (Package, error) {
|
||||
localDate, err := currentLocalDate(briefingPackage.Metadata.GeneratedAt, briefingPackage.Metadata.Timezone)
|
||||
func Build(req BuildRequest) (Package, error) {
|
||||
localDate, err := currentLocalDate(req.Metadata.GeneratedAt, req.Metadata.Timezone)
|
||||
if err != nil {
|
||||
return Package{}, err
|
||||
}
|
||||
items := make([]changes.Change, len(recentChanges))
|
||||
copy(items, recentChanges)
|
||||
items := make([]changes.Change, len(req.RecentChanges))
|
||||
copy(items, req.RecentChanges)
|
||||
if items == nil {
|
||||
items = []changes.Change{}
|
||||
}
|
||||
pkg := Package{
|
||||
SchemaVersion: SchemaVersion,
|
||||
RunID: briefingPackage.Metadata.RunID,
|
||||
RunID: req.Metadata.RunID,
|
||||
Report: Report{
|
||||
ID: briefingPackage.Metadata.ReportID,
|
||||
Variant: briefingPackage.Metadata.Variant,
|
||||
PromptID: briefingPackage.Metadata.PromptID,
|
||||
GeneratedAt: briefingPackage.Metadata.GeneratedAt,
|
||||
Timezone: briefingPackage.Metadata.Timezone,
|
||||
ID: req.Metadata.ReportID,
|
||||
Variant: req.Metadata.Variant,
|
||||
PromptID: req.Metadata.PromptID,
|
||||
GeneratedAt: req.Metadata.GeneratedAt,
|
||||
Timezone: req.Metadata.Timezone,
|
||||
CurrentLocalDate: localDate,
|
||||
ValidPeriod: briefingPackage.Metadata.ValidPeriod,
|
||||
ValidPeriod: req.Metadata.ValidPeriod,
|
||||
},
|
||||
Briefing: briefingPackage,
|
||||
Briefing: stanzasFromSnapshot(req.Modules),
|
||||
RecentChanges: RecentChanges{Items: items},
|
||||
SourceWarnings: briefingPackage.Metadata.SourceWarnings,
|
||||
SourceWarnings: append([]weatherdata.SourceWarning(nil), req.Metadata.SourceWarnings...),
|
||||
}
|
||||
if err := Validate(pkg); err != nil {
|
||||
return Package{}, err
|
||||
@@ -74,6 +95,16 @@ func BuildWithRecentChanges(briefingPackage briefing.Package, recentChanges []ch
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func stanzasFromSnapshot(snapshot module.Snapshot) BriefingStanzas {
|
||||
values := map[string]any{}
|
||||
order := make([]string, 0, len(snapshot.Outputs))
|
||||
for _, output := range snapshot.Outputs {
|
||||
order = append(order, output.StanzaName)
|
||||
values[output.StanzaName] = output.Value
|
||||
}
|
||||
return BriefingStanzas{Order: order, Values: values}
|
||||
}
|
||||
|
||||
func currentLocalDate(generatedAt time.Time, timezone string) (string, error) {
|
||||
location, err := timeutil.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
@@ -86,6 +117,9 @@ func Validate(pkg Package) error {
|
||||
if pkg.SchemaVersion == "" {
|
||||
return fmt.Errorf("schemaVersion is required")
|
||||
}
|
||||
if pkg.SchemaVersion != SchemaVersion {
|
||||
return fmt.Errorf("schemaVersion must be %s", SchemaVersion)
|
||||
}
|
||||
if pkg.RunID == "" {
|
||||
return fmt.Errorf("runId is required")
|
||||
}
|
||||
@@ -107,33 +141,134 @@ func Validate(pkg Package) error {
|
||||
if !pkg.Report.ValidPeriod.IsValid() {
|
||||
return fmt.Errorf("report.validPeriod must be valid")
|
||||
}
|
||||
if pkg.Briefing.Metadata.RunID == "" {
|
||||
return fmt.Errorf("briefing.metadata.runId is required")
|
||||
if len(pkg.Briefing.Order) == 0 {
|
||||
return fmt.Errorf("briefing stanzas are required")
|
||||
}
|
||||
if pkg.Briefing.Metadata.RunID != pkg.RunID {
|
||||
return fmt.Errorf("briefing.metadata.runId must match runId")
|
||||
}
|
||||
if pkg.Briefing.Metadata.SchemaVersion == "" {
|
||||
return fmt.Errorf("briefing.metadata.schemaVersion is required")
|
||||
}
|
||||
if pkg.Briefing.Metadata.PromptID != pkg.Report.PromptID {
|
||||
return fmt.Errorf("briefing.metadata.promptId must match report.promptId")
|
||||
}
|
||||
if pkg.Briefing.Metadata.ReportID != pkg.Report.ID {
|
||||
return fmt.Errorf("briefing.metadata.reportId must match report.id")
|
||||
}
|
||||
if pkg.Briefing.Daily == nil && pkg.Briefing.ThreeDay == nil && pkg.Briefing.Weekend == nil && pkg.Briefing.Storm == nil {
|
||||
return fmt.Errorf("briefing report content is required")
|
||||
for _, name := range pkg.Briefing.Order {
|
||||
if name == "" {
|
||||
return fmt.Errorf("briefing stanza name is required")
|
||||
}
|
||||
if _, ok := pkg.Briefing.Values[name]; !ok {
|
||||
return fmt.Errorf("briefing stanza %q is missing", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Save(path string, pkg Package) error {
|
||||
if err := Validate(pkg); err != nil {
|
||||
data, err := MarshalYAML(pkg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileutil.WriteJSONAtomic(path, pkg); err != nil {
|
||||
if err := fileutil.WriteFileAtomic(path, data); err != nil {
|
||||
return fmt.Errorf("save data package: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MarshalYAML(pkg Package) ([]byte, error) {
|
||||
if err := Validate(pkg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := yaml.Marshal(pkg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal data package: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func LoadYAML(data []byte) (Package, error) {
|
||||
var pkg Package
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
if err := decoder.Decode(&pkg); err != nil {
|
||||
return Package{}, fmt.Errorf("decode data package: %w", err)
|
||||
}
|
||||
if err := Validate(pkg); err != nil {
|
||||
return Package{}, err
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (b BriefingStanzas) MarshalYAML() (any, error) {
|
||||
node := &yaml.Node{Kind: yaml.MappingNode}
|
||||
for _, name := range b.Order {
|
||||
value, ok := b.Values[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: name}
|
||||
valueNode, err := yamlNode(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal briefing stanza %q: %w", name, err)
|
||||
}
|
||||
node.Content = append(node.Content, keyNode, valueNode)
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (b *BriefingStanzas) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("briefing must be a mapping")
|
||||
}
|
||||
values := map[string]any{}
|
||||
order := make([]string, 0, len(value.Content)/2)
|
||||
for i := 0; i < len(value.Content); i += 2 {
|
||||
name := value.Content[i].Value
|
||||
var stanza any
|
||||
if err := value.Content[i+1].Decode(&stanza); err != nil {
|
||||
return err
|
||||
}
|
||||
order = append(order, name)
|
||||
values[name] = stanza
|
||||
}
|
||||
b.Order = order
|
||||
b.Values = values
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BriefingStanzas) MarshalJSON() ([]byte, error) {
|
||||
out := map[string]any{}
|
||||
for _, name := range b.Order {
|
||||
if value, ok := b.Values[name]; ok {
|
||||
out[name] = value
|
||||
}
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func (b *BriefingStanzas) UnmarshalJSON(data []byte) error {
|
||||
var values map[string]any
|
||||
if err := json.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
order := make([]string, 0, len(values))
|
||||
for name := range values {
|
||||
order = append(order, name)
|
||||
}
|
||||
b.Order = order
|
||||
b.Values = values
|
||||
return nil
|
||||
}
|
||||
|
||||
func yamlNode(value any) (*yaml.Node, error) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var normalized any
|
||||
if err := json.Unmarshal(data, &normalized); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err = yaml.Marshal(normalized)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var node yaml.Node
|
||||
if err := yaml.Unmarshal(data, &node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(node.Content) == 0 {
|
||||
return &yaml.Node{Kind: yaml.MappingNode}, nil
|
||||
}
|
||||
return node.Content[0], nil
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package promptinput
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
func TestBuildDailyDataPackage(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
|
||||
pkg, err := Build(briefingPackage)
|
||||
pkg, err := Build(validBuildRequest(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
@@ -23,7 +20,7 @@ func TestBuildDailyDataPackage(t *testing.T) {
|
||||
t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion)
|
||||
}
|
||||
if pkg.RunID != "20260529T100000Z_daily_today" {
|
||||
t.Fatalf("RunID = %q, want briefing run id", pkg.RunID)
|
||||
t.Fatalf("RunID = %q, want metadata run id", pkg.RunID)
|
||||
}
|
||||
if pkg.Report.PromptID != "weather.daily_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.daily_report", pkg.Report.PromptID)
|
||||
@@ -31,14 +28,11 @@ func TestBuildDailyDataPackage(t *testing.T) {
|
||||
if pkg.Report.CurrentLocalDate != "2026-05-29" {
|
||||
t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate)
|
||||
}
|
||||
if pkg.Briefing.Daily == nil {
|
||||
t.Fatal("Briefing.Daily = nil")
|
||||
if pkg.Briefing.Order[0] != "metadata" || pkg.Briefing.Order[1] != "current_conditions" || pkg.Briefing.Order[2] != "derived_daily_summary" {
|
||||
t.Fatalf("Briefing.Order = %#v, want snapshot stanza order", pkg.Briefing.Order)
|
||||
}
|
||||
if pkg.Briefing.Metadata.Location == nil || pkg.Briefing.Metadata.Location.Name != "Brentwood" {
|
||||
t.Fatalf("Briefing.Metadata.Location = %#v, want configured location", pkg.Briefing.Metadata.Location)
|
||||
}
|
||||
if pkg.Briefing.CurrentConditions == nil || pkg.Briefing.CurrentConditions.ConditionText != "Partly cloudy" {
|
||||
t.Fatalf("Briefing.CurrentConditions = %#v, want current conditions", pkg.Briefing.CurrentConditions)
|
||||
if got := pkg.Briefing.Values["current_conditions"].(map[string]string)["condition_text"]; got != "Partly cloudy" {
|
||||
t.Fatalf("current_conditions.condition_text = %q, want Partly cloudy", got)
|
||||
}
|
||||
if pkg.RecentChanges.Items == nil || len(pkg.RecentChanges.Items) != 0 {
|
||||
t.Fatalf("RecentChanges.Items = %#v, want empty slice", pkg.RecentChanges.Items)
|
||||
@@ -46,11 +40,11 @@ func TestBuildDailyDataPackage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildCurrentLocalDateUsesReportTimezone(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
briefingPackage.Metadata.GeneratedAt = time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC)
|
||||
briefingPackage.Metadata.Timezone = "America/Chicago"
|
||||
req := validBuildRequest(t)
|
||||
req.Metadata.GeneratedAt = time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC)
|
||||
req.Metadata.Timezone = "America/Chicago"
|
||||
|
||||
pkg, err := Build(briefingPackage)
|
||||
pkg, err := Build(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
@@ -61,10 +55,10 @@ func TestBuildCurrentLocalDateUsesReportTimezone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildRejectsInvalidReportTimezone(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
briefingPackage.Metadata.Timezone = "Not/AZone"
|
||||
req := validBuildRequest(t)
|
||||
req.Metadata.Timezone = "Not/AZone"
|
||||
|
||||
_, err := Build(briefingPackage)
|
||||
_, err := Build(req)
|
||||
if err == nil {
|
||||
t.Fatal("Build() error = nil, want invalid timezone error")
|
||||
}
|
||||
@@ -74,7 +68,7 @@ func TestBuildRejectsInvalidReportTimezone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateRequiresFields(t *testing.T) {
|
||||
pkg, err := Build(validBriefingPackage())
|
||||
pkg, err := Build(validBuildRequest(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
@@ -90,7 +84,7 @@ func TestValidateRequiresFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateRequiresCurrentLocalDate(t *testing.T) {
|
||||
pkg, err := Build(validBriefingPackage())
|
||||
pkg, err := Build(validBuildRequest(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
@@ -105,17 +99,17 @@ func TestValidateRequiresCurrentLocalDate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildThreeDayDataPackage(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
briefingPackage.Metadata.RunID = "20260529T100000Z_three_day"
|
||||
briefingPackage.Metadata.ReportID = report.ThreeDay
|
||||
briefingPackage.Metadata.PromptID = "weather.three_day_outlook"
|
||||
briefingPackage.Daily = nil
|
||||
briefingPackage.ThreeDay = &briefing.ThreeDay{
|
||||
Days: []briefing.OutlookDay{{Date: "2026-05-29"}},
|
||||
}
|
||||
func TestBuildUsesNamedSnapshotStanzas(t *testing.T) {
|
||||
req := validBuildRequest(t)
|
||||
req.Metadata.RunID = "20260529T100000Z_three_day"
|
||||
req.Metadata.ReportID = report.ThreeDay
|
||||
req.Metadata.PromptID = "weather.three_day_outlook"
|
||||
req.Modules = snapshotWithOutputs(t,
|
||||
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}},
|
||||
module.Output{ID: module.ForecastDelta, StanzaName: "three_day", Value: map[string]any{"days": []string{"2026-05-29"}}},
|
||||
)
|
||||
|
||||
pkg, err := Build(briefingPackage)
|
||||
pkg, err := Build(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
@@ -123,107 +117,88 @@ func TestBuildThreeDayDataPackage(t *testing.T) {
|
||||
if pkg.Report.ID != report.ThreeDay {
|
||||
t.Fatalf("Report.ID = %q, want three_day", pkg.Report.ID)
|
||||
}
|
||||
if pkg.Briefing.ThreeDay == nil {
|
||||
t.Fatal("Briefing.ThreeDay = nil")
|
||||
if _, ok := pkg.Briefing.Values["three_day"]; !ok {
|
||||
t.Fatal("Briefing.Values[three_day] missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWeekendDataPackage(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
briefingPackage.Metadata.RunID = "20260529T100000Z_weekend"
|
||||
briefingPackage.Metadata.ReportID = report.Weekend
|
||||
briefingPackage.Metadata.PromptID = "weather.weekend_outlook"
|
||||
briefingPackage.Daily = nil
|
||||
briefingPackage.Weekend = &briefing.Weekend{
|
||||
Days: []briefing.OutlookDay{{Date: "2026-05-30"}},
|
||||
}
|
||||
|
||||
pkg, err := Build(briefingPackage)
|
||||
func TestMarshalYAMLIsDeterministicAndUsesNamedStanzas(t *testing.T) {
|
||||
pkg, err := Build(validBuildRequest(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
|
||||
if pkg.Report.ID != report.Weekend {
|
||||
t.Fatalf("Report.ID = %q, want weekend", pkg.Report.ID)
|
||||
}
|
||||
if pkg.Briefing.Weekend == nil {
|
||||
t.Fatal("Briefing.Weekend = nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildStormDataPackage(t *testing.T) {
|
||||
briefingPackage := validBriefingPackage()
|
||||
briefingPackage.Metadata.RunID = "20260529T100000Z_storm"
|
||||
briefingPackage.Metadata.ReportID = report.Storm
|
||||
briefingPackage.Metadata.PromptID = "weather.storm_report"
|
||||
briefingPackage.Daily = nil
|
||||
briefingPackage.Storm = &briefing.Storm{
|
||||
TimingWindow: briefingPackage.Metadata.ValidPeriod,
|
||||
Hazards: []string{"Thunderstorms"},
|
||||
}
|
||||
|
||||
pkg, err := Build(briefingPackage)
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
|
||||
if pkg.Report.ID != report.Storm {
|
||||
t.Fatalf("Report.ID = %q, want storm", pkg.Report.ID)
|
||||
}
|
||||
if pkg.Report.PromptID != "weather.storm_report" {
|
||||
t.Fatalf("PromptID = %q, want weather.storm_report", pkg.Report.PromptID)
|
||||
}
|
||||
if pkg.Briefing.Storm == nil {
|
||||
t.Fatal("Briefing.Storm = nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalDeterministic(t *testing.T) {
|
||||
pkg, err := Build(validBriefingPackage())
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
|
||||
first, err := json.MarshalIndent(pkg, "", " ")
|
||||
first, err := MarshalYAML(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("first marshal: %v", err)
|
||||
}
|
||||
second, err := json.MarshalIndent(pkg, "", " ")
|
||||
second, err := MarshalYAML(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("second marshal: %v", err)
|
||||
}
|
||||
if string(first) != string(second) {
|
||||
t.Fatalf("JSON output changed between marshals:\n%s\n---\n%s", string(first), string(second))
|
||||
t.Fatalf("YAML output changed between marshals:\n%s\n---\n%s", string(first), string(second))
|
||||
}
|
||||
if !strings.Contains(string(first), "schema_version: weatherreporter.data_package.v2") ||
|
||||
!strings.Contains(string(first), "briefing:\n") ||
|
||||
!strings.Contains(string(first), " current_conditions:\n") ||
|
||||
!strings.Contains(string(first), " condition_text: Partly cloudy") {
|
||||
t.Fatalf("YAML output missing expected named stanzas:\n%s", string(first))
|
||||
}
|
||||
}
|
||||
|
||||
func validBriefingPackage() briefing.Package {
|
||||
func TestLoadYAMLRoundTrip(t *testing.T) {
|
||||
pkg, err := Build(validBuildRequest(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
data, err := MarshalYAML(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalYAML() error = %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadYAML(data)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadYAML() error = %v", err)
|
||||
}
|
||||
|
||||
if loaded.SchemaVersion != SchemaVersion || loaded.RunID != pkg.RunID {
|
||||
t.Fatalf("loaded package = %#v, want schema and run id", loaded)
|
||||
}
|
||||
if loaded.Briefing.Order[1] != "current_conditions" {
|
||||
t.Fatalf("loaded briefing order = %#v, want current_conditions second", loaded.Briefing.Order)
|
||||
}
|
||||
}
|
||||
|
||||
func validBuildRequest(t *testing.T) BuildRequest {
|
||||
t.Helper()
|
||||
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
|
||||
return briefing.Package{
|
||||
Metadata: briefing.Metadata{
|
||||
SchemaVersion: briefing.SchemaVersion,
|
||||
RunID: "20260529T100000Z_daily_today",
|
||||
ReportID: report.DailyToday,
|
||||
PromptID: "weather.daily_report",
|
||||
GeneratedAt: generatedAt,
|
||||
Units: "us",
|
||||
Timezone: "America/Chicago",
|
||||
Location: &briefing.LocationContext{
|
||||
ID: "home",
|
||||
Name: "Brentwood",
|
||||
Region: "St. Louis Metro",
|
||||
Timezone: "America/Chicago",
|
||||
},
|
||||
return BuildRequest{
|
||||
Metadata: Metadata{
|
||||
RunID: "20260529T100000Z_daily_today",
|
||||
ReportID: report.DailyToday,
|
||||
Variant: "today",
|
||||
PromptID: "weather.daily_report",
|
||||
GeneratedAt: generatedAt,
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: timeutil.Period{
|
||||
Start: time.Date(2026, 5, 29, 5, 0, 0, 0, time.UTC),
|
||||
End: time.Date(2026, 5, 30, 5, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
CurrentConditions: &briefing.CurrentConditionsContext{
|
||||
ConditionText: "Partly cloudy",
|
||||
},
|
||||
Daily: &briefing.Daily{
|
||||
ForecastSummaryDate: "2026-05-29",
|
||||
},
|
||||
Modules: snapshotWithOutputs(t,
|
||||
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily_today"}},
|
||||
module.Output{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]string{"condition_text": "Partly cloudy"}},
|
||||
module.Output{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]string{"date": "2026-05-29"}},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func snapshotWithOutputs(t *testing.T, outputs ...module.Output) module.Snapshot {
|
||||
t.Helper()
|
||||
snapshot, err := module.NewSnapshot(outputs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSnapshot() error = %v", err)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user