package app import ( "bytes" "reflect" "testing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/collect" "gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" ) func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) { cfg := generationConfig() bundle := generationBundle(t) resolved, err := ResolveGenerate(GenerateRequest{ Config: cfg, Report: ReportDaily, Date: generationTime("2026-05-29T12:00:00-05:00"), Now: generationTime("2026-05-29T08:30:00-05:00"), }, generationTime("2026-05-29T08:30:00-05:00")) if err != nil { t.Fatalf("ResolveGenerate() error = %v", err) } request := prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}, handler: preparedHandler(t, resolved)} prepared, err := prepareReport(request) if err != nil { t.Fatalf("prepareReport() error = %v", err) } repeated, err := prepareReport(request) if err != nil { t.Fatalf("second prepareReport() error = %v", err) } if len(prepared.dataPackage) == 0 || !bytes.Equal(prepared.dataPackage, repeated.dataPackage) || !reflect.DeepEqual(prepared.identity, repeated.identity) { t.Fatalf("prepared package and identity are not deterministic: %q/%#v", prepared.dataPackage, prepared.identity) } originalDataPackage := append([]byte(nil), prepared.dataPackage...) originalIdentity := prepared.identity originalDerived := prepared.derived originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...) identity, snapshot, derived, err := prepared.renderInputs() if err != nil { t.Fatalf("renderInputs() error = %v", err) } identity.SourceWarnings = append(identity.SourceWarnings, weatherdata.SourceWarning{Source: "test", Message: "consumer mutation"}) snapshot.Outputs = nil derived.PrecipTiming.ThunderMentioned = false bundle.Hourly.Periods[0].TextDescription = "mutated after preparation" bundle.Warnings = append(bundle.Warnings, weatherdata.SourceWarning{Source: "test", Message: "mutated warning"}) if len(bundle.Sources) > 0 { if bundle.Sources[0].Query == nil { bundle.Sources[0].Query = map[string]string{} } bundle.Sources[0].Query["mutated"] = "true" } if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.derived, originalDerived) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) { t.Fatalf("prepared values changed after caller mutation: %#v", prepared) } if len(prepared.moduleSnapshot.Outputs) == 0 { t.Fatal("prepared report values retain consumer mutation") } } func TestPrepareReportProjectsPreparedIdentity(t *testing.T) { cfg := generationConfig() bundle := generationBundle(t) resolved, err := ResolveGenerate(GenerateRequest{ Config: cfg, Report: ReportDaily, Date: generationTime("2026-05-29T12:00:00-05:00"), Now: generationTime("2026-05-29T08:30:00-05:00"), }, generationTime("2026-05-29T08:30:00-05:00")) if err != nil { t.Fatalf("ResolveGenerate() error = %v", err) } prepared, err := prepareReport(prepareReportRequest{Config: cfg, Resolved: resolved, Collection: collect.Result{Bundle: &bundle}, handler: preparedHandler(t, resolved)}) if err != nil { t.Fatalf("prepareReport() error = %v", err) } identity := prepared.identity renderIdentity, _, _, err := prepared.renderInputs() if err != nil { t.Fatalf("renderInputs() error = %v", err) } if !reflect.DeepEqual(renderIdentity, identity) { t.Fatalf("render identity = %#v, want %#v", renderIdentity, identity) } prompt := promptMetadata(identity) if prompt.RunID != identity.RunID || prompt.ReportID != identity.ReportID || prompt.Variant != identity.Variant || prompt.PromptID != identity.PromptID || !prompt.GeneratedAt.Equal(identity.GeneratedAt) || prompt.Timezone != identity.Timezone || prompt.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(prompt.SourceWarnings, identity.SourceWarnings) { t.Fatalf("prompt metadata does not match prepared identity: %#v/%#v", prompt, identity) } moduleMetadata, found, err := module.StanzaValue[briefing.MetadataModule](prepared.moduleSnapshot, "metadata") if err != nil || !found { t.Fatalf("metadata stanza = %#v/%t/%v", moduleMetadata, found, err) } if moduleMetadata.RunID != identity.RunID || moduleMetadata.ReportID != identity.ReportID || moduleMetadata.Variant != identity.Variant || moduleMetadata.PromptID != identity.PromptID || !moduleMetadata.GeneratedAt.Equal(identity.GeneratedAt) || moduleMetadata.Units != identity.Units || moduleMetadata.Timezone != identity.Timezone || moduleMetadata.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(moduleMetadata.Location, identity.Location) { t.Fatalf("module metadata does not match prepared identity: %#v/%#v", moduleMetadata, identity) } if len(moduleMetadata.SourceWarnings) != len(identity.SourceWarnings) { t.Fatalf("module source warnings = %#v, want %#v", moduleMetadata.SourceWarnings, identity.SourceWarnings) } for index, warning := range identity.SourceWarnings { summary := moduleMetadata.SourceWarnings[index] if summary.Source != warning.Source || summary.Code != warning.Code || summary.Severity != warning.Severity || summary.Message != warning.Message || summary.CompletenessImpact != warning.CompletenessImpact { t.Fatalf("module source warning %d = %#v, want %#v", index, summary, warning) } } } func preparedHandler(t *testing.T, resolved report.Resolved) generatedtext.Handler { t.Helper() handler, err := generatedtext.LookupDefinition(resolved.Definition) if err != nil { t.Fatalf("LookupDefinition() error = %v", err) } return handler }