Validate render identity and daypart fallbacks

This commit is contained in:
2026-08-13 02:11:25 +00:00
parent 360c665a3e
commit 2bd921f247
12 changed files with 253 additions and 184 deletions

View File

@@ -7,6 +7,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/collect"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
@@ -18,7 +19,7 @@ import (
// executions for one resolved report.
type preparedReport struct {
resolved report.Resolved
reportFacts ReportFacts
derived facts.DerivedFacts
moduleSnapshot module.Snapshot
identity briefing.PreparedIdentity
sourceWarnings []weatherdata.SourceWarning
@@ -73,9 +74,9 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
return preparedReport{}, &preparationError{operation: "lookup generated text catalog", err: err}
}
clonedFacts, err := clonePreparedValue(reportFacts)
clonedDerived, err := clonePreparedValue(reportFacts.Derived)
if err != nil {
return preparedReport{}, &preparationError{operation: "copy prepared report facts", err: err}
return preparedReport{}, &preparationError{operation: "copy prepared derived facts", err: err}
}
clonedSnapshot, err := clonePreparedValue(moduleSnapshot)
if err != nil {
@@ -87,7 +88,7 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
}
prepared := preparedReport{
resolved: cloneResolved(req.Resolved),
reportFacts: clonedFacts,
derived: clonedDerived,
moduleSnapshot: clonedSnapshot,
identity: clonedIdentity,
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
@@ -120,20 +121,20 @@ func (p preparedReport) sourceWarningsCopy() []weatherdata.SourceWarning {
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
}
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, ReportFacts, error) {
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, facts.DerivedFacts, error) {
identity, err := clonePreparedValue(p.identity)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
snapshot, err := clonePreparedValue(p.moduleSnapshot)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
reportFacts, err := clonePreparedValue(p.reportFacts)
derived, err := clonePreparedValue(p.derived)
if err != nil {
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
return briefing.PreparedIdentity{}, module.Snapshot{}, facts.DerivedFacts{}, err
}
return identity, snapshot, reportFacts, nil
return identity, snapshot, derived, nil
}
func clonePreparedValue[T any](value T) (T, error) {

View File

@@ -37,14 +37,15 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
originalDataPackage := append([]byte(nil), prepared.dataPackage...)
originalIdentity := prepared.identity
originalDerived := prepared.derived
originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...)
identity, snapshot, reportFacts, err := prepared.renderInputs()
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
reportFacts.Collected.Hourly.Periods[0].TextDescription = "consumer mutation"
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 {
@@ -54,13 +55,10 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
bundle.Sources[0].Query["mutated"] = "true"
}
if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) {
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 prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "mutated after preparation" {
t.Fatal("prepared report facts retain caller-owned weather data")
}
if prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "consumer mutation" || len(prepared.moduleSnapshot.Outputs) == 0 {
if len(prepared.moduleSnapshot.Outputs) == 0 {
t.Fatal("prepared report values retain consumer mutation")
}
}

View File

@@ -115,11 +115,11 @@ func executePreparedProfile(ctx context.Context, req profileExecutionRequest) (p
if err != nil {
return outcome, nil, &profileExecutionError{operation: "validate generated text", err: err}
}
identity, snapshot, reportFacts, err := req.Prepared.renderInputs()
identity, snapshot, derived, err := req.Prepared.renderInputs()
if err != nil {
return outcome, nil, &profileExecutionError{operation: "copy prepared render inputs", err: err}
}
renderContext, err := req.Prepared.handler.BuildRenderContext(identity, snapshot, reportFacts.Collected, reportFacts.Derived, generatedText)
renderContext, err := req.Prepared.handler.BuildRenderContext(identity, snapshot, derived, generatedText)
if err != nil {
return outcome, nil, &profileExecutionError{operation: "build render context", err: err}
}