Centralize generated text template catalog

This commit is contained in:
2026-06-15 12:24:47 +00:00
parent ff2f8c16a3
commit a02af0bce0
7 changed files with 364 additions and 114 deletions

View File

@@ -103,17 +103,19 @@ For `scriptorium_markdown` reports, generation then:
For `generated_text_template` reports, generation then: For `generated_text_template` reports, generation then:
12. Runs structured Scriptorium generation to the raw generated-text JSON path. 12. Looks up the generated-text catalog entry for the report schema/template
13. Saves the structured Scriptorium run result. IDs.
14. Validates and saves normalized generated text. 13. Runs structured Scriptorium generation to the raw generated-text JSON path.
15. Builds and saves a typed render context. 14. Saves the structured Scriptorium run result.
16. Renders Markdown from the embedded template to the managed report path. 15. Validates and saves normalized generated text.
17. Saves final metadata with generated-text paths, render context path, schema 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. ID, and managed report path.
18. Copies the managed report to the requested `--out` path when provided. 19. Copies the managed report to the requested `--out` path when provided.
19. If distributor notification is enabled, notifies using the managed report 20. If distributor notification is enabled, notifies using the managed report
path as the source file. 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. its path.
If render preflight returns both a result and an error, preflight JSON and If render preflight returns both a result and an error, preflight JSON and

View File

@@ -7,7 +7,9 @@ This document describes structured generated-text handling in
`internal/generatedtext` validates structured text returned for `internal/generatedtext` validates structured text returned for
generated-text-template reports and builds curated render contexts 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 ## Inputs And Outputs
@@ -25,6 +27,8 @@ Outputs:
- normalized stable JSON for validated generated text - normalized stable JSON for validated generated text
- typed `TomorrowRenderContext` values for `internal/reporttemplate` - typed `TomorrowRenderContext` values for `internal/reporttemplate`
- typed `HourlyRenderContext` 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: The hourly generated text JSON accepts:
@@ -60,10 +64,12 @@ JSON when blank.
## Boundaries ## Boundaries
- This package owns typed generated-text validation and render-context shaping. - 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 uses typed module snapshot decoding through `module.StanzaValue`.
- It does not invoke Scriptorium, write state artifacts, choose report - It does not invoke Scriptorium, write state artifacts, choose report
definitions, compare snapshots, or render templates directly in production definitions, compare snapshots, or own embedded template/schema files.
workflows. - 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 - 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 limited to typed JSON decoding, unknown-field rejection, and required-field
checks. checks.
@@ -77,6 +83,8 @@ JSON when blank.
- Missing optional render-context stanzas become nil module pointers. - Missing optional render-context stanzas become nil module pointers.
- Invalid render metadata, including missing timezone, missing generated time, - Invalid render metadata, including missing timezone, missing generated time,
or invalid valid period, fails before template rendering. 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 ## Tests
@@ -84,11 +92,14 @@ Inspect:
- `internal/generatedtext/hourly_test.go` - `internal/generatedtext/hourly_test.go`
- `internal/generatedtext/tomorrow_test.go` - `internal/generatedtext/tomorrow_test.go`
- `internal/generatedtext/catalog_test.go`
- `internal/generatedtext/render_context_test.go` - `internal/generatedtext/render_context_test.go`
## Invariants ## Invariants
- Render contexts are curated structs, not raw prompt-input packages. - Render contexts are curated structs, not raw prompt-input packages.
- Required generated text is normalized before downstream artifact storage. - 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 - Missing optional weather narrative stanzas produce empty or fallback render
context fields rather than forcing raw module data into templates. context fields rather than forcing raw module data into templates.

View File

@@ -39,8 +39,9 @@ validate GeneratedText, construct render contexts, choose report definitions,
write artifacts, invoke Scriptorium, or notify distributor. write artifacts, invoke Scriptorium, or notify distributor.
GeneratedText validation is owned by `internal/generatedtext`. App GeneratedText validation is owned by `internal/generatedtext`. App
orchestration decides which template and schema IDs apply to a report through orchestration uses `internal/generatedtext` catalog lookup to connect
`internal/report` definitions. `internal/report` definition schema/template IDs to the matching validator,
render-context builder, and embedded assets.
## Template Contracts ## Template Contracts

View File

@@ -21,7 +21,6 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/module" "gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "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/state"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata" "gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
@@ -654,6 +653,11 @@ type generatedReportRequest struct {
} }
func generateTextTemplateReport(ctx context.Context, req generatedReportRequest) (*ReportResult, error) { 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{ structuredResult, runErr := req.renderer.StructuredRun(ctx, scriptorium.StructuredRunRequest{
PromptID: req.Resolved.Definition.PromptID, PromptID: req.Resolved.Definition.PromptID,
DataPackagePath: req.dataPackagePath, DataPackagePath: req.dataPackagePath,
@@ -680,7 +684,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
if err != nil { if err != nil {
return nil, generatedReportError(req.Resolved, req.metadata.RunID, "load raw generated text", err) 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 { if err != nil {
return nil, generatedReportError(req.Resolved, req.metadata.RunID, "validate generated text", err) 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 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 { if err != nil {
return nil, generatedReportError(req.Resolved, req.metadata.RunID, "build render context", err) 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 return nil, err
} }
rendered, err := reporttemplate.Render(req.Resolved.Definition.TemplateID, renderContext) rendered, err := handler.Render(renderContext)
if err != nil { if err != nil {
return nil, generatedReportError(req.Resolved, req.metadata.RunID, "render template", err) 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 { func generatedReportError(resolved report.Resolved, runID string, operation string, err error) error {
if err == nil { if err == nil {
return nil return nil

View File

@@ -16,7 +16,6 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium" "gitea.maximumdirect.net/eric/weatherreporter/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "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/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "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) cfg, resolved, store, notifier, outputPath := hourlyGeneratedTextFixture(t)
resolved.Definition.TemplateID = "missing-template" resolved.Definition.TemplateID = "missing-template"
renderer := &recordingRenderer{ renderer := &recordingRenderer{
@@ -837,77 +836,21 @@ func TestGenerateHourlyReportRejectsUnsupportedTemplateBeforeRenderContext(t *te
Store: store, Store: store,
Notifier: notifier, 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) assertNoGeneratedFailureSideEffects(t, notifier, outputPath)
paths := hourlyArtifactPaths(t, store, resolved) paths := hourlyArtifactPaths(t, store, resolved)
assertPathsExist(t, paths.Preflight, paths.Metadata, paths.GeneratedTextRaw, paths.GeneratedTextResult, paths.GeneratedText) assertPathsExist(t, paths.Preflight, paths.Metadata)
assertPathsMissing(t, paths.RenderContext, paths.RenderedReport) assertPathsMissing(t, paths.GeneratedTextRaw, paths.GeneratedTextResult, paths.GeneratedText, paths.RenderContext, paths.RenderedReport)
metadataData, readErr := os.ReadFile(paths.Metadata) metadataData, readErr := os.ReadFile(paths.Metadata)
if readErr != nil { if readErr != nil {
t.Fatalf("read metadata: %v", readErr) t.Fatalf("read metadata: %v", readErr)
} }
if !strings.Contains(string(metadataData), paths.GeneratedText) { if !strings.Contains(string(metadataData), paths.GeneratedTextRaw) {
t.Fatalf("metadata missing generated text link:\n%s", string(metadataData)) t.Fatalf("metadata missing generated text artifact path:\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)
} }
} }

View File

@@ -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)
}

View File

@@ -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)
}
}