370 lines
13 KiB
Go
370 lines
13 KiB
Go
package promptinput
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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) {
|
|
pkg, err := Build(validBuildRequest(t))
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
|
|
if pkg.SchemaVersion != SchemaVersion {
|
|
t.Fatalf("SchemaVersion = %q, want %q", pkg.SchemaVersion, SchemaVersion)
|
|
}
|
|
if pkg.RunID != "20260529T100000Z_daily" {
|
|
t.Fatalf("RunID = %q, want metadata run id", pkg.RunID)
|
|
}
|
|
if pkg.Report.PromptID != "weather.daily_generated_text" {
|
|
t.Fatalf("PromptID = %q, want weather.daily_generated_text", pkg.Report.PromptID)
|
|
}
|
|
if pkg.Report.CurrentLocalDate != "2026-05-29" {
|
|
t.Fatalf("CurrentLocalDate = %q, want 2026-05-29", pkg.Report.CurrentLocalDate)
|
|
}
|
|
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 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)
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentLocalDateUsesReportTimezone(t *testing.T) {
|
|
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(req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
|
|
if pkg.Report.CurrentLocalDate != "2026-05-29" {
|
|
t.Fatalf("CurrentLocalDate = %q, want local Chicago date 2026-05-29", pkg.Report.CurrentLocalDate)
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsInvalidReportTimezone(t *testing.T) {
|
|
req := validBuildRequest(t)
|
|
req.Metadata.Timezone = "Not/AZone"
|
|
|
|
_, err := Build(req)
|
|
if err == nil {
|
|
t.Fatal("Build() error = nil, want invalid timezone error")
|
|
}
|
|
if !strings.Contains(err.Error(), "report timezone") {
|
|
t.Fatalf("error = %q, want report timezone context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresFields(t *testing.T) {
|
|
pkg, err := Build(validBuildRequest(t))
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
pkg.RunID = ""
|
|
|
|
err = Validate(pkg)
|
|
if err == nil {
|
|
t.Fatal("Validate() error = nil, want required field error")
|
|
}
|
|
if !strings.Contains(err.Error(), "runId") {
|
|
t.Fatalf("error = %q, want runId context", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresCurrentLocalDate(t *testing.T) {
|
|
pkg, err := Build(validBuildRequest(t))
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
pkg.Report.CurrentLocalDate = ""
|
|
|
|
err = Validate(pkg)
|
|
if err == nil {
|
|
t.Fatal("Validate() error = nil, want required field error")
|
|
}
|
|
if !strings.Contains(err.Error(), "currentLocalDate") {
|
|
t.Fatalf("error = %q, want currentLocalDate context", err.Error())
|
|
}
|
|
}
|
|
|
|
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.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{"days": []string{"2026-05-29"}}},
|
|
module.Output{ID: module.TodayPlanning, StanzaName: "today_planning", Value: map[string]any{"morning_readiness": []string{"routine"}}},
|
|
)
|
|
|
|
pkg, err := Build(req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
|
|
if pkg.Report.ID != report.ThreeDay {
|
|
t.Fatalf("Report.ID = %q, want three_day", pkg.Report.ID)
|
|
}
|
|
if _, ok := pkg.Briefing.Values["derived_daypart_summaries"]; !ok {
|
|
t.Fatal("Briefing.Values[derived_daypart_summaries] missing")
|
|
}
|
|
if _, ok := pkg.Briefing.Values["today_planning"]; !ok {
|
|
t.Fatal("Briefing.Values[today_planning] missing")
|
|
}
|
|
}
|
|
|
|
func TestBuildUsesPromptValuesFromSnapshot(t *testing.T) {
|
|
req := validBuildRequest(t)
|
|
req.Modules = snapshotWithOutputs(t,
|
|
module.Output{
|
|
ID: module.Metadata,
|
|
StanzaName: "metadata",
|
|
Value: map[string]string{"run_id": "rich"},
|
|
PromptValue: map[string]string{"run_id": "prompt"},
|
|
},
|
|
module.Output{
|
|
ID: module.CurrentConditions,
|
|
StanzaName: "current_conditions",
|
|
Value: map[string]string{"condition_text": "Rich conditions"},
|
|
PromptValue: map[string]string{"condition_text": "Prompt conditions"},
|
|
},
|
|
module.Output{
|
|
ID: module.AlertDigest,
|
|
StanzaName: "alert_digest",
|
|
Value: map[string]bool{"checked": true},
|
|
},
|
|
)
|
|
|
|
pkg, err := Build(req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
|
|
if got := pkg.Briefing.Values["metadata"].(map[string]string)["run_id"]; got != "prompt" {
|
|
t.Fatalf("metadata.run_id = %q, want prompt value", got)
|
|
}
|
|
if got := pkg.Briefing.Values["current_conditions"].(map[string]string)["condition_text"]; got != "Prompt conditions" {
|
|
t.Fatalf("current_conditions.condition_text = %q, want prompt value", got)
|
|
}
|
|
if got := pkg.Briefing.Values["alert_digest"].(map[string]bool)["checked"]; !got {
|
|
t.Fatalf("alert_digest.checked = %v, want rich value fallback", got)
|
|
}
|
|
}
|
|
|
|
func TestMarshalYAMLIsDeterministicAndGroupsNamedStanzas(t *testing.T) {
|
|
pkg, err := Build(validBuildRequest(t))
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
|
|
first, err := MarshalYAML(pkg)
|
|
if err != nil {
|
|
t.Fatalf("first marshal: %v", err)
|
|
}
|
|
second, err := MarshalYAML(pkg)
|
|
if err != nil {
|
|
t.Fatalf("second marshal: %v", err)
|
|
}
|
|
if 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.v3") ||
|
|
!strings.Contains(string(first), "briefing:\n") ||
|
|
!strings.Contains(string(first), " applicable_risk_products:\n") ||
|
|
!strings.Contains(string(first), " derived_summaries:\n") ||
|
|
!strings.Contains(string(first), " narrative_products:\n") ||
|
|
!strings.Contains(string(first), " raw_data:\n") ||
|
|
!strings.Contains(string(first), " current_conditions:\n") ||
|
|
!strings.Contains(string(first), " condition_text: Partly cloudy") {
|
|
t.Fatalf("YAML output missing expected grouped stanzas:\n%s", string(first))
|
|
}
|
|
for _, pair := range []struct {
|
|
before string
|
|
after string
|
|
}{
|
|
{before: " metadata:\n", after: " applicable_risk_products:\n"},
|
|
{before: " applicable_risk_products:\n", after: " derived_summaries:\n"},
|
|
{before: " derived_summaries:\n", after: " narrative_products:\n"},
|
|
{before: " narrative_products:\n", after: " raw_data:\n"},
|
|
} {
|
|
if strings.Index(string(first), pair.before) < 0 || strings.Index(string(first), pair.after) < 0 || strings.Index(string(first), pair.before) > strings.Index(string(first), pair.after) {
|
|
t.Fatalf("YAML category order is wrong, want %q before %q:\n%s", pair.before, pair.after, string(first))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarshalYAMLPlacesSPCConvectiveStanzasInPromptCategories(t *testing.T) {
|
|
req := validBuildRequest(t)
|
|
req.Modules = snapshotWithOutputs(t,
|
|
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": req.Metadata.RunID}},
|
|
module.Output{ID: module.SPCConvectiveDiscussion, StanzaName: string(module.SPCConvectiveDiscussion), Value: map[string]any{"discussions": []string{"day1"}}},
|
|
module.Output{ID: module.SPCConvectiveOutlooks, StanzaName: string(module.SPCConvectiveOutlooks), Value: map[string]any{"outlook_count": 1}},
|
|
)
|
|
|
|
pkg, err := Build(req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
data, err := MarshalYAML(pkg)
|
|
if err != nil {
|
|
t.Fatalf("MarshalYAML() error = %v", err)
|
|
}
|
|
text := string(data)
|
|
|
|
riskIndex := strings.Index(text, " applicable_risk_products:\n")
|
|
outlookIndex := strings.Index(text, " spc_convective_outlooks:\n")
|
|
narrativeIndex := strings.Index(text, " narrative_products:\n")
|
|
discussionIndex := strings.Index(text, " spc_convective_discussion:\n")
|
|
if riskIndex < 0 || outlookIndex < 0 || narrativeIndex < 0 || discussionIndex < 0 {
|
|
t.Fatalf("YAML output missing SPC convective category placement:\n%s", text)
|
|
}
|
|
if !(riskIndex < outlookIndex && outlookIndex < narrativeIndex && narrativeIndex < discussionIndex) {
|
|
t.Fatalf("YAML output placed SPC convective stanzas in wrong order:\n%s", text)
|
|
}
|
|
if strings.Contains(text, " raw_data:\n spc_convective") || strings.Contains(text, " derived_summaries:\n spc_convective") {
|
|
t.Fatalf("YAML output placed SPC convective stanzas in wrong category:\n%s", text)
|
|
}
|
|
|
|
loaded, err := LoadYAML(data)
|
|
if err != nil {
|
|
t.Fatalf("LoadYAML() error = %v", err)
|
|
}
|
|
if _, ok := loaded.Briefing.Values[string(module.SPCConvectiveOutlooks)]; !ok {
|
|
t.Fatal("loaded package missing spc_convective_outlooks stanza")
|
|
}
|
|
if _, ok := loaded.Briefing.Values[string(module.SPCConvectiveDiscussion)]; !ok {
|
|
t.Fatal("loaded package missing spc_convective_discussion stanza")
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
wantOrder := []string{"metadata", "alert_digest", "derived_daily_summary", "narrative_forecast", "current_conditions"}
|
|
if strings.Join(loaded.Briefing.Order, ",") != strings.Join(wantOrder, ",") {
|
|
t.Fatalf("loaded package order = %#v, want grouped category order %#v", loaded.Briefing.Order, wantOrder)
|
|
}
|
|
if got := loaded.Briefing.Values["current_conditions"].(map[string]any)["condition_text"]; got != "Partly cloudy" {
|
|
t.Fatalf("loaded current_conditions.condition_text = %#v, want Partly cloudy", got)
|
|
}
|
|
}
|
|
|
|
func TestMarshalYAMLRejectsUncategorizedStanza(t *testing.T) {
|
|
req := validBuildRequest(t)
|
|
req.Modules = snapshotWithOutputs(t, module.Output{ID: module.ID("custom"), StanzaName: "custom", Value: map[string]string{"value": "x"}})
|
|
|
|
_, err := Build(req)
|
|
if err == nil || !strings.Contains(err.Error(), `briefing stanza "custom" has no prompt-input category`) {
|
|
t.Fatalf("Build() error = %v, want uncategorized stanza error", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadYAMLRejectsMisplacedStanza(t *testing.T) {
|
|
data := []byte(`
|
|
schema_version: weatherreporter.data_package.v3
|
|
run_id: 20260529T100000Z_daily
|
|
report:
|
|
id: daily
|
|
prompt_id: weather.daily_generated_text
|
|
generated_at: 2026-05-29T10:00:00Z
|
|
timezone: America/Chicago
|
|
current_local_date: "2026-05-29"
|
|
valid_period:
|
|
start: 2026-05-29T05:00:00Z
|
|
end: 2026-05-30T05:00:00Z
|
|
briefing:
|
|
metadata:
|
|
run_id: 20260529T100000Z_daily
|
|
raw_data:
|
|
alert_digest:
|
|
checked: true
|
|
recent_changes:
|
|
items: []
|
|
`)
|
|
|
|
_, err := LoadYAML(data)
|
|
if err == nil || !strings.Contains(err.Error(), `briefing stanza "alert_digest" belongs under category "applicable_risk_products"`) {
|
|
t.Fatalf("LoadYAML() error = %v, want misplaced stanza error", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadYAMLRejectsOldSchemaVersion(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)
|
|
}
|
|
data = []byte(strings.Replace(string(data), "weatherreporter.data_package.v3", "weatherreporter.data_package.v2", 1))
|
|
|
|
_, err = LoadYAML(data)
|
|
if err == nil || !strings.Contains(err.Error(), "schemaVersion must be weatherreporter.data_package.v3") {
|
|
t.Fatalf("LoadYAML() error = %v, want current schema version error", err)
|
|
}
|
|
}
|
|
|
|
func validBuildRequest(t *testing.T) BuildRequest {
|
|
t.Helper()
|
|
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
|
|
return BuildRequest{
|
|
Metadata: Metadata{
|
|
RunID: "20260529T100000Z_daily",
|
|
ReportID: report.Daily,
|
|
Variant: "today",
|
|
PromptID: "weather.daily_generated_text",
|
|
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),
|
|
},
|
|
},
|
|
Modules: snapshotWithOutputs(t,
|
|
module.Output{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": "20260529T100000Z_daily"}},
|
|
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"}},
|
|
module.Output{ID: module.AlertDigest, StanzaName: "alert_digest", Value: map[string]bool{"checked": true}},
|
|
module.Output{ID: module.NarrativeForecast, StanzaName: "narrative_forecast", Value: map[string]string{"product": "narrative"}},
|
|
),
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|