Centralize generated text template catalog
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
155
internal/generatedtext/catalog.go
Normal file
155
internal/generatedtext/catalog.go
Normal 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)
|
||||
}
|
||||
164
internal/generatedtext/catalog_test.go
Normal file
164
internal/generatedtext/catalog_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user