From a02af0bce05b3435d40d4479306bb677c7442c96 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 15 Jun 2026 12:24:47 +0000 Subject: [PATCH] Centralize generated text template catalog --- docs/internal/app-orchestration.md | 20 +-- docs/internal/generatedtext.md | 17 ++- docs/internal/reporttemplate.md | 5 +- internal/app/app.go | 42 ++----- internal/app/app_test.go | 75 ++--------- internal/generatedtext/catalog.go | 155 +++++++++++++++++++++++ internal/generatedtext/catalog_test.go | 164 +++++++++++++++++++++++++ 7 files changed, 364 insertions(+), 114 deletions(-) create mode 100644 internal/generatedtext/catalog.go create mode 100644 internal/generatedtext/catalog_test.go diff --git a/docs/internal/app-orchestration.md b/docs/internal/app-orchestration.md index b2017b7..f2b799a 100644 --- a/docs/internal/app-orchestration.md +++ b/docs/internal/app-orchestration.md @@ -103,17 +103,19 @@ For `scriptorium_markdown` reports, generation then: For `generated_text_template` reports, generation then: -12. Runs structured Scriptorium generation to the raw generated-text JSON path. -13. Saves the structured Scriptorium run result. -14. Validates and saves normalized generated text. -15. Builds and saves a typed render context. -16. Renders Markdown from the embedded template to the managed report path. -17. Saves final metadata with generated-text paths, render context path, schema +12. Looks up the generated-text catalog entry for the report schema/template + IDs. +13. Runs structured Scriptorium generation to the raw generated-text JSON path. +14. Saves the structured Scriptorium run result. +15. Validates and saves normalized generated text. +16. Builds and saves a typed render context. +17. Renders Markdown from the embedded template to the managed report path. +18. Saves final metadata with generated-text paths, render context path, schema ID, and managed report path. -18. Copies the managed report to the requested `--out` path when provided. -19. If distributor notification is enabled, notifies using the managed report +19. Copies the managed report to the requested `--out` path when provided. +20. If distributor notification is enabled, notifies using the managed report path as the source file. -20. Saves a distributor notification debug artifact and updates metadata with +21. Saves a distributor notification debug artifact and updates metadata with its path. If render preflight returns both a result and an error, preflight JSON and diff --git a/docs/internal/generatedtext.md b/docs/internal/generatedtext.md index ea806b6..cb5ff26 100644 --- a/docs/internal/generatedtext.md +++ b/docs/internal/generatedtext.md @@ -7,7 +7,9 @@ This document describes structured generated-text handling in `internal/generatedtext` validates structured text returned for generated-text-template reports and builds curated render contexts for -templates. The implemented contracts are Tomorrow Report and Hourly Report. +templates. It also owns the generated-text catalog that connects report +definitions to validators, render-context builders, schema assets, and template +assets. The implemented contracts are Tomorrow Report and Hourly Report. ## Inputs And Outputs @@ -25,6 +27,8 @@ Outputs: - normalized stable JSON for validated generated text - typed `TomorrowRenderContext` values for `internal/reporttemplate` - typed `HourlyRenderContext` values for `internal/reporttemplate` +- generated-text catalog handlers for report definitions that use + `generated_text_template` The hourly generated text JSON accepts: @@ -60,10 +64,12 @@ JSON when blank. ## Boundaries - This package owns typed generated-text validation and render-context shaping. +- It owns generated-text catalog lookup for schema/template combinations. - It uses typed module snapshot decoding through `module.StanzaValue`. - It does not invoke Scriptorium, write state artifacts, choose report - definitions, compare snapshots, or render templates directly in production - workflows. + definitions, compare snapshots, or own embedded template/schema files. +- It renders through `internal/reporttemplate`; embedded asset lookup remains + in `internal/reporttemplate`. - It does not use a Go JSON Schema dependency; schema enforcement in Go is limited to typed JSON decoding, unknown-field rejection, and required-field checks. @@ -77,6 +83,8 @@ JSON when blank. - Missing optional render-context stanzas become nil module pointers. - Invalid render metadata, including missing timezone, missing generated time, or invalid valid period, fails before template rendering. +- Unsupported generated-text schema IDs, template IDs, or schema/template + combinations fail during catalog lookup with report ID context. ## Tests @@ -84,11 +92,14 @@ Inspect: - `internal/generatedtext/hourly_test.go` - `internal/generatedtext/tomorrow_test.go` +- `internal/generatedtext/catalog_test.go` - `internal/generatedtext/render_context_test.go` ## Invariants - Render contexts are curated structs, not raw prompt-input packages. - Required generated text is normalized before downstream artifact storage. +- Generated-text-template reports must have one catalog entry matching their + report definition schema and template IDs. - Missing optional weather narrative stanzas produce empty or fallback render context fields rather than forcing raw module data into templates. diff --git a/docs/internal/reporttemplate.md b/docs/internal/reporttemplate.md index 974753c..deedb0a 100644 --- a/docs/internal/reporttemplate.md +++ b/docs/internal/reporttemplate.md @@ -39,8 +39,9 @@ validate GeneratedText, construct render contexts, choose report definitions, write artifacts, invoke Scriptorium, or notify distributor. GeneratedText validation is owned by `internal/generatedtext`. App -orchestration decides which template and schema IDs apply to a report through -`internal/report` definitions. +orchestration uses `internal/generatedtext` catalog lookup to connect +`internal/report` definition schema/template IDs to the matching validator, +render-context builder, and embedded assets. ## Template Contracts diff --git a/internal/app/app.go b/internal/app/app.go index 4114d86..77c7683 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -21,7 +21,6 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" - "gitea.maximumdirect.net/eric/weatherreporter/internal/reporttemplate" "gitea.maximumdirect.net/eric/weatherreporter/internal/state" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" @@ -654,6 +653,11 @@ type generatedReportRequest struct { } func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) (*ReportResult, error) { + handler, err := generatedtext.LookupDefinition(req.Resolved.Definition) + if err != nil { + return nil, generatedReportError(req.Resolved, req.metadata.RunID, "lookup generated text catalog", err) + } + structuredResult, runErr := req.renderer.StructuredRun(ctx, scriptorium.StructuredRunRequest{ PromptID: req.Resolved.Definition.PromptID, DataPackagePath: req.dataPackagePath, @@ -680,7 +684,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) if err != nil { return nil, generatedReportError(req.Resolved, req.metadata.RunID, "load raw generated text", err) } - generatedText, normalizedGeneratedText, err := validateGeneratedText(req.Resolved.Definition, rawGeneratedText) + generatedText, normalizedGeneratedText, err := handler.Validate(rawGeneratedText) if err != nil { return nil, generatedReportError(req.Resolved, req.metadata.RunID, "validate generated text", err) } @@ -694,7 +698,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) return nil, err } - renderContext, err := buildRenderContext(req.Resolved.Definition, req.briefingMetadata, req.moduleSnapshot, req.reportFacts, generatedText) + renderContext, err := handler.BuildRenderContext(req.briefingMetadata, req.moduleSnapshot, req.reportFacts.Collected, req.reportFacts.Derived, generatedText) if err != nil { return nil, generatedReportError(req.Resolved, req.metadata.RunID, "build render context", err) } @@ -708,7 +712,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) return nil, err } - rendered, err := reporttemplate.Render(req.Resolved.Definition.TemplateID, renderContext) + rendered, err := handler.Render(renderContext) if err != nil { return nil, generatedReportError(req.Resolved, req.metadata.RunID, "render template", err) } @@ -1113,36 +1117,6 @@ func preflightArtifact(result *scriptorium.RenderResult) state.PreflightArtifact } } -func validateGeneratedText(definition report.Definition, data []byte) (any, []byte, error) { - switch definition.GeneratedTextSchemaID { - case "hourly": - return generatedtext.ValidateHourly(data) - case "tomorrow": - return generatedtext.ValidateTomorrow(data) - default: - return nil, nil, fmt.Errorf("generated text schema %q is not supported for report %q", definition.GeneratedTextSchemaID, definition.ID) - } -} - -func buildRenderContext(definition report.Definition, metadata briefing.Metadata, snapshot module.Snapshot, reportFacts ReportFacts, generated any) (any, error) { - switch definition.TemplateID { - case "hourly": - hourly, ok := generated.(generatedtext.Hourly) - if !ok { - return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", definition.TemplateID, definition.ID) - } - return generatedtext.BuildHourlyRenderContext(metadata, snapshot, hourly, reportFacts.Collected, reportFacts.Derived) - case "tomorrow": - tomorrow, ok := generated.(generatedtext.Tomorrow) - if !ok { - return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", definition.TemplateID, definition.ID) - } - return generatedtext.BuildTomorrowRenderContext(metadata, snapshot, tomorrow, reportFacts.Collected, reportFacts.Derived) - default: - return nil, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID) - } -} - func generatedReportError(resolved report.Resolved, runID string, operation string, err error) error { if err == nil { return nil diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 6759f2d..3370e2b 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -16,7 +16,6 @@ import ( "gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/config" - "gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext" "gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/report" @@ -820,7 +819,7 @@ func TestGenerateHourlyReportPreservesRawTextOnValidationFailure(t *testing.T) { } } -func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeRenderContext(t *testing.T) { +func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeStructuredRun(t *testing.T) { cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t) resolved.Definition.TemplateID = "missing-template" renderer := &recordingRenderer{ @@ -837,77 +836,21 @@ func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeRenderContext(t *te Store: store, Notifier: notifier, }) - assertGeneratedReportError(t, err, resolved, "build render context") + assertGeneratedReportError(t, err, resolved, "lookup generated text catalog") + if renderer.structuredRunCalls != 0 || renderer.runCalls != 0 { + t.Fatalf("calls structured=%d run=%d, want no generated text run", renderer.structuredRunCalls, renderer.runCalls) + } assertNoGeneratedFailureSideEffects(t, notifier, outputPath) paths := hourlyArtifactPaths(t, store, resolved) - assertPathsExist(t, paths.Preflight, paths.Metadata, paths.GeneratedTextRaw, paths.GeneratedTextResult, paths.GeneratedText) - assertPathsMissing(t, paths.RenderContext, paths.RenderedReport) + assertPathsExist(t, paths.Preflight, paths.Metadata) + assertPathsMissing(t, paths.GeneratedTextRaw, paths.GeneratedTextResult, paths.GeneratedText, paths.RenderContext, paths.RenderedReport) metadataData, readErr := os.ReadFile(paths.Metadata) if readErr != nil { t.Fatalf("read metadata: %v", readErr) } - if !strings.Contains(string(metadataData), paths.GeneratedText) { - t.Fatalf("metadata missing generated text link:\n%s", string(metadataData)) - } -} - -func TestGeneratedTextValidationDispatchSupportsKnownSchemas(t *testing.T) { - hourlyDefinition := report.DefaultRegistry().MustLookup(report.Hourly) - hourly, normalized, err := validateGeneratedText(hourlyDefinition, []byte(`{ - "summary": " Storm chances increase. ", - "forecast_discussion": " A front will keep the region unsettled. " - }`)) - if err != nil { - t.Fatalf("validateGeneratedText(hourly) error = %v", err) - } - if _, ok := hourly.(generatedtext.Hourly); !ok { - t.Fatalf("hourly generated text type = %T, want generatedtext.Hourly", hourly) - } - if !strings.Contains(string(normalized), `"summary":"Storm chances increase."`) { - t.Fatalf("hourly normalized text = %s, want trimmed summary", normalized) - } - - tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow) - tomorrow, normalized, err := validateGeneratedText(tomorrowDefinition, []byte(`{ - "summary": " Storms become more likely tomorrow. ", - "forecast_discussion": [" A front will keep showers in the forecast. ", ""] - }`)) - if err != nil { - t.Fatalf("validateGeneratedText(tomorrow) error = %v", err) - } - if _, ok := tomorrow.(generatedtext.Tomorrow); !ok { - t.Fatalf("tomorrow generated text type = %T, want generatedtext.Tomorrow", tomorrow) - } - if !strings.Contains(string(normalized), `"forecast_discussion":["A front will keep showers in the forecast."]`) { - t.Fatalf("tomorrow normalized text = %s, want trimmed discussion paragraph", normalized) - } -} - -func TestBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) { - definition := report.DefaultRegistry().MustLookup(report.Hourly) - - _, err := buildRenderContext(definition, briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, generatedtext.Tomorrow{ - Summary: "Storms become more likely tomorrow.", - ForecastDiscussion: []string{"A front will keep showers in the forecast."}, - }) - if err == nil { - t.Fatal("buildRenderContext() error = nil, want type mismatch") - } - if !strings.Contains(err.Error(), `requires hourly generated text`) { - t.Fatalf("buildRenderContext() error = %v, want hourly generated text requirement", err) - } - - tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow) - _, err = buildRenderContext(tomorrowDefinition, briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, generatedtext.Hourly{ - Summary: "Storm chances increase.", - ForecastDiscussion: "A front will keep showers in the forecast.", - }) - if err == nil { - t.Fatal("buildRenderContext() error = nil, want tomorrow type mismatch") - } - if !strings.Contains(err.Error(), `requires tomorrow generated text`) { - t.Fatalf("buildRenderContext() error = %v, want tomorrow generated text requirement", err) + if !strings.Contains(string(metadataData), paths.GeneratedTextRaw) { + t.Fatalf("metadata missing generated text artifact path:\n%s", string(metadataData)) } } diff --git a/internal/generatedtext/catalog.go b/internal/generatedtext/catalog.go new file mode 100644 index 0000000..4ec784d --- /dev/null +++ b/internal/generatedtext/catalog.go @@ -0,0 +1,155 @@ +package generatedtext + +import ( + "fmt" + + "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" + "gitea.maximumdirect.net/eric/weatherreporter/internal/facts" + "gitea.maximumdirect.net/eric/weatherreporter/internal/module" + "gitea.maximumdirect.net/eric/weatherreporter/internal/report" + "gitea.maximumdirect.net/eric/weatherreporter/internal/reporttemplate" +) + +const ( + schemaIDHourly = "hourly" + schemaIDTomorrow = "tomorrow" + + templateIDHourly = "hourly" + templateIDTomorrow = "tomorrow" +) + +type validator func([]byte) (any, []byte, error) + +type renderContextBuilder func(report.ID, string, briefing.Metadata, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error) + +type catalogEntry struct { + schemaID string + templateID string + validate validator + buildRenderContext renderContextBuilder +} + +type Handler struct { + reportID report.ID + schemaID string + templateID string + validate validator + buildRenderContext renderContextBuilder +} + +var catalog = []catalogEntry{ + { + schemaID: schemaIDHourly, + templateID: templateIDHourly, + validate: validateHourly, + buildRenderContext: buildHourlyContext, + }, + { + schemaID: schemaIDTomorrow, + templateID: templateIDTomorrow, + validate: validateTomorrow, + buildRenderContext: buildTomorrowContext, + }, +} + +func LookupDefinition(definition report.Definition) (Handler, error) { + if definition.GenerationMode != report.GenerationModeGeneratedTextTemplate { + return Handler{}, fmt.Errorf("report %q uses generation mode %q, not %q", definition.ID, definition.GenerationMode, report.GenerationModeGeneratedTextTemplate) + } + + var schemaKnown, templateKnown bool + for _, entry := range catalog { + if entry.schemaID == definition.GeneratedTextSchemaID { + schemaKnown = true + } + if entry.templateID == definition.TemplateID { + templateKnown = true + } + if entry.schemaID == definition.GeneratedTextSchemaID && entry.templateID == definition.TemplateID { + return Handler{ + reportID: definition.ID, + schemaID: entry.schemaID, + templateID: entry.templateID, + validate: entry.validate, + buildRenderContext: entry.buildRenderContext, + }, nil + } + } + if !schemaKnown { + return Handler{}, fmt.Errorf("generated text schema %q is not supported for report %q", definition.GeneratedTextSchemaID, definition.ID) + } + if !templateKnown { + return Handler{}, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID) + } + return Handler{}, fmt.Errorf("generated text schema %q and report template %q are not supported together for report %q", definition.GeneratedTextSchemaID, definition.TemplateID, definition.ID) +} + +func (h Handler) SchemaID() string { + return h.schemaID +} + +func (h Handler) TemplateID() string { + return h.templateID +} + +func (h Handler) Schema() ([]byte, error) { + data, err := reporttemplate.Schema(h.schemaID) + if err != nil { + return nil, fmt.Errorf("load generated text schema %q for report %q: %w", h.schemaID, h.reportID, err) + } + return data, nil +} + +func (h Handler) Template() (string, error) { + source, err := reporttemplate.Template(h.templateID) + if err != nil { + return "", fmt.Errorf("load report template %q for report %q: %w", h.templateID, h.reportID, err) + } + return source, nil +} + +func (h Handler) Validate(data []byte) (any, []byte, error) { + if h.validate == nil { + return nil, nil, fmt.Errorf("generated text validator is not registered for schema %q on report %q", h.schemaID, h.reportID) + } + return h.validate(data) +} + +func (h Handler) BuildRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) { + if h.buildRenderContext == nil { + return nil, fmt.Errorf("render-context builder is not registered for template %q on report %q", h.templateID, h.reportID) + } + return h.buildRenderContext(h.reportID, h.templateID, metadata, snapshot, collected, derived, generated) +} + +func (h Handler) Render(data any) ([]byte, error) { + rendered, err := reporttemplate.Render(h.templateID, data) + if err != nil { + return nil, fmt.Errorf("render report template %q for report %q: %w", h.templateID, h.reportID, err) + } + return rendered, nil +} + +func validateHourly(data []byte) (any, []byte, error) { + return ValidateHourly(data) +} + +func validateTomorrow(data []byte) (any, []byte, error) { + return ValidateTomorrow(data) +} + +func buildHourlyContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) { + hourly, ok := generated.(Hourly) + if !ok { + return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", templateID, reportID) + } + return BuildHourlyRenderContext(metadata, snapshot, hourly, collected, derived) +} + +func buildTomorrowContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) { + tomorrow, ok := generated.(Tomorrow) + if !ok { + return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", templateID, reportID) + } + return BuildTomorrowRenderContext(metadata, snapshot, tomorrow, collected, derived) +} diff --git a/internal/generatedtext/catalog_test.go b/internal/generatedtext/catalog_test.go new file mode 100644 index 0000000..18240fb --- /dev/null +++ b/internal/generatedtext/catalog_test.go @@ -0,0 +1,164 @@ +package generatedtext + +import ( + "strings" + "testing" + + "gitea.maximumdirect.net/eric/weatherreporter/internal/report" +) + +func TestCatalogCompleteForGeneratedTextTemplateReports(t *testing.T) { + for _, definition := range report.DefaultRegistry().All() { + if definition.GenerationMode != report.GenerationModeGeneratedTextTemplate { + continue + } + t.Run(string(definition.ID), func(t *testing.T) { + handler, err := LookupDefinition(definition) + if err != nil { + t.Fatalf("LookupDefinition() error = %v", err) + } + if handler.SchemaID() != definition.GeneratedTextSchemaID { + t.Fatalf("SchemaID() = %q, want %q", handler.SchemaID(), definition.GeneratedTextSchemaID) + } + if handler.TemplateID() != definition.TemplateID { + t.Fatalf("TemplateID() = %q, want %q", handler.TemplateID(), definition.TemplateID) + } + if handler.validate == nil { + t.Fatal("validator is nil") + } + if handler.buildRenderContext == nil { + t.Fatal("render-context builder is nil") + } + if schema, err := handler.Schema(); err != nil { + t.Fatalf("Schema() error = %v", err) + } else if len(schema) == 0 { + t.Fatal("Schema() returned empty asset") + } + if template, err := handler.Template(); err != nil { + t.Fatalf("Template() error = %v", err) + } else if template == "" { + t.Fatal("Template() returned empty asset") + } + }) + } +} + +func TestCatalogLookupRejectsUnsupportedSchemaAndTemplate(t *testing.T) { + tests := []struct { + name string + definition report.Definition + want []string + }{ + { + name: "schema", + definition: func() report.Definition { + definition := report.DefaultRegistry().MustLookup(report.Hourly) + definition.GeneratedTextSchemaID = "missing-schema" + return definition + }(), + want: []string{`generated text schema "missing-schema"`, `report "hourly"`}, + }, + { + name: "template", + definition: func() report.Definition { + definition := report.DefaultRegistry().MustLookup(report.Hourly) + definition.TemplateID = "missing-template" + return definition + }(), + want: []string{`report template "missing-template"`, `report "hourly"`}, + }, + { + name: "combination", + definition: func() report.Definition { + definition := report.DefaultRegistry().MustLookup(report.Hourly) + definition.TemplateID = "tomorrow" + return definition + }(), + want: []string{`generated text schema "hourly"`, `report template "tomorrow"`, `report "hourly"`}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := LookupDefinition(test.definition) + if err == nil { + t.Fatal("LookupDefinition() error = nil, want error") + } + for _, want := range test.want { + if !strings.Contains(err.Error(), want) { + t.Fatalf("LookupDefinition() error = %v, want %q", err, want) + } + } + }) + } +} + +func TestCatalogValidationDispatchSupportsKnownSchemas(t *testing.T) { + hourlyHandler, err := LookupDefinition(report.DefaultRegistry().MustLookup(report.Hourly)) + if err != nil { + t.Fatalf("LookupDefinition(hourly) error = %v", err) + } + hourly, normalized, err := hourlyHandler.Validate([]byte(`{ + "summary": " Storm chances increase. ", + "forecast_discussion": " A front will keep the region unsettled. " + }`)) + if err != nil { + t.Fatalf("Validate(hourly) error = %v", err) + } + if _, ok := hourly.(Hourly); !ok { + t.Fatalf("hourly generated text type = %T, want generatedtext.Hourly", hourly) + } + if !strings.Contains(string(normalized), `"summary":"Storm chances increase."`) { + t.Fatalf("hourly normalized text = %s, want trimmed summary", normalized) + } + + tomorrowHandler, err := LookupDefinition(report.DefaultRegistry().MustLookup(report.Tomorrow)) + if err != nil { + t.Fatalf("LookupDefinition(tomorrow) error = %v", err) + } + tomorrow, normalized, err := tomorrowHandler.Validate([]byte(`{ + "summary": " Storms become more likely tomorrow. ", + "forecast_discussion": [" A front will keep showers in the forecast. ", ""] + }`)) + if err != nil { + t.Fatalf("Validate(tomorrow) error = %v", err) + } + if _, ok := tomorrow.(Tomorrow); !ok { + t.Fatalf("tomorrow generated text type = %T, want generatedtext.Tomorrow", tomorrow) + } + if !strings.Contains(string(normalized), `"forecast_discussion":["A front will keep showers in the forecast."]`) { + t.Fatalf("tomorrow normalized text = %s, want trimmed discussion paragraph", normalized) + } +} + +func TestCatalogBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) { + hourlyHandler, err := LookupDefinition(report.DefaultRegistry().MustLookup(report.Hourly)) + if err != nil { + t.Fatalf("LookupDefinition(hourly) error = %v", err) + } + _, err = hourlyHandler.BuildRenderContext(testMetadata(), testSnapshot(t), testCollected(), testDerived(), Tomorrow{ + Summary: "Storms become more likely tomorrow.", + ForecastDiscussion: []string{"A front will keep showers in the forecast."}, + }) + if err == nil { + t.Fatal("BuildRenderContext(hourly) error = nil, want type mismatch") + } + if !strings.Contains(err.Error(), `requires hourly generated text`) { + t.Fatalf("BuildRenderContext(hourly) error = %v, want hourly generated text requirement", err) + } + + tomorrowHandler, err := LookupDefinition(report.DefaultRegistry().MustLookup(report.Tomorrow)) + if err != nil { + t.Fatalf("LookupDefinition(tomorrow) error = %v", err) + } + _, err = tomorrowHandler.BuildRenderContext(testTomorrowMetadata(), testTomorrowSnapshot(t), testCollected(), testTomorrowDerived(), Hourly{ + Summary: "Storm chances increase.", + ForecastDiscussion: "A front will keep showers in the forecast.", + }) + if err == nil { + t.Fatal("BuildRenderContext(tomorrow) error = nil, want type mismatch") + } + if !strings.Contains(err.Error(), `requires tomorrow generated text`) { + t.Fatalf("BuildRenderContext(tomorrow) error = %v, want tomorrow generated text requirement", err) + } +}