Attach prompt export values in module registry

This commit is contained in:
2026-06-15 20:25:12 +00:00
parent ff6aade42c
commit 92fcbfcc05
2 changed files with 196 additions and 0 deletions

View File

@@ -21,6 +21,8 @@ type ModuleContext struct {
type ModuleBuilder func(ModuleContext, any) (*module.Output, error)
type ModulePromptExporter func(value any) (any, error)
type ModuleDefinition struct {
ID module.ID
StanzaName string
@@ -31,6 +33,7 @@ type ModuleDefinition struct {
MissingData module.MissingDataBehavior
AllowDuplicate bool
Builder ModuleBuilder
PromptExporter ModulePromptExporter
}
type ModuleRegistry struct {
@@ -130,6 +133,15 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
if output.StanzaName != definition.StanzaName {
return nil, fmt.Errorf("module %q produced stanza %q, want %q", definition.ID, output.StanzaName, definition.StanzaName)
}
if definition.PromptExporter == nil {
output.PromptValue = output.Value
return output, nil
}
promptValue, err := definition.PromptExporter(output.Value)
if err != nil {
return nil, fmt.Errorf("module %q stanza %q prompt export: %w", definition.ID, definition.StanzaName, err)
}
output.PromptValue = promptValue
return output, nil
}

View File

@@ -1,6 +1,8 @@
package briefing
import (
"encoding/json"
"errors"
"strings"
"testing"
@@ -40,6 +42,9 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
t.Fatalf("BuildModule(%s) error = %v", item.ID, err)
}
if output != nil {
if output.DataPackageValue() == nil {
t.Fatalf("BuildModule(%s) data package value = nil", item.ID)
}
outputs = append(outputs, *output)
}
}
@@ -54,6 +59,173 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
}
}
func TestModuleRegistryAddsPassThroughPromptValue(t *testing.T) {
registry, err := NewModuleRegistry([]ModuleDefinition{
{
ID: module.Metadata,
StanzaName: "metadata",
Builder: func(ModuleContext, any) (*module.Output, error) {
return &module.Output{
ID: module.Metadata,
StanzaName: "metadata",
Value: testRegistryValue{Message: "rich"},
}, nil
},
},
})
if err != nil {
t.Fatalf("NewModuleRegistry() error = %v", err)
}
output, err := registry.BuildModule(testRegistryModuleContext(), module.ConfigItem{ID: module.Metadata})
if err != nil {
t.Fatalf("BuildModule() error = %v", err)
}
if output == nil {
t.Fatal("BuildModule() output = nil, want output")
}
if output.PromptValue != output.Value {
t.Fatalf("PromptValue = %#v, want pass-through rich value %#v", output.PromptValue, output.Value)
}
if output.DataPackageValue() != output.Value {
t.Fatalf("DataPackageValue() = %#v, want rich value", output.DataPackageValue())
}
}
func TestModuleRegistryAddsCustomPromptValue(t *testing.T) {
registry, err := NewModuleRegistry([]ModuleDefinition{
{
ID: module.Metadata,
StanzaName: "metadata",
Builder: func(ModuleContext, any) (*module.Output, error) {
return &module.Output{
ID: module.Metadata,
StanzaName: "metadata",
Value: testRegistryValue{Message: "rich"},
}, nil
},
PromptExporter: func(value any) (any, error) {
rich, ok := value.(testRegistryValue)
if !ok {
return nil, errors.New("unexpected rich value type")
}
return testRegistryValue{Message: rich.Message + " prompt"}, nil
},
},
})
if err != nil {
t.Fatalf("NewModuleRegistry() error = %v", err)
}
output, err := registry.BuildModule(testRegistryModuleContext(), module.ConfigItem{ID: module.Metadata})
if err != nil {
t.Fatalf("BuildModule() error = %v", err)
}
got, ok := output.PromptValue.(testRegistryValue)
if !ok {
t.Fatalf("PromptValue type = %T, want testRegistryValue", output.PromptValue)
}
if got.Message != "rich prompt" {
t.Fatalf("PromptValue = %#v, want custom prompt value", got)
}
if output.DataPackageValue() != output.PromptValue {
t.Fatalf("DataPackageValue() = %#v, want custom prompt value", output.DataPackageValue())
}
if output.Value.(testRegistryValue).Message != "rich" {
t.Fatalf("Value = %#v, want rich value unchanged", output.Value)
}
}
func TestModuleRegistryWrapsPromptExporterErrors(t *testing.T) {
registry, err := NewModuleRegistry([]ModuleDefinition{
{
ID: module.Metadata,
StanzaName: "metadata",
Builder: func(ModuleContext, any) (*module.Output, error) {
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: testRegistryValue{Message: "rich"}}, nil
},
PromptExporter: func(any) (any, error) {
return nil, errors.New("unsupported value")
},
},
})
if err != nil {
t.Fatalf("NewModuleRegistry() error = %v", err)
}
_, err = registry.BuildModule(testRegistryModuleContext(), module.ConfigItem{ID: module.Metadata})
if err == nil ||
!strings.Contains(err.Error(), `module "metadata" stanza "metadata" prompt export`) ||
!strings.Contains(err.Error(), "unsupported value") {
t.Fatalf("BuildModule() error = %v, want wrapped exporter error", err)
}
}
func TestModuleRegistryValidatesOutputBeforePromptExport(t *testing.T) {
called := false
registry, err := NewModuleRegistry([]ModuleDefinition{
{
ID: module.Metadata,
StanzaName: "metadata",
Builder: func(ModuleContext, any) (*module.Output, error) {
return &module.Output{ID: module.CurrentConditions, StanzaName: "metadata", Value: testRegistryValue{Message: "rich"}}, nil
},
PromptExporter: func(any) (any, error) {
called = true
return testRegistryValue{Message: "prompt"}, nil
},
},
})
if err != nil {
t.Fatalf("NewModuleRegistry() error = %v", err)
}
_, err = registry.BuildModule(testRegistryModuleContext(), module.ConfigItem{ID: module.Metadata})
if err == nil || !strings.Contains(err.Error(), `module "metadata" produced output id "current_conditions"`) {
t.Fatalf("BuildModule() error = %v, want output id validation error", err)
}
if called {
t.Fatal("PromptExporter called before output validation")
}
}
func TestModuleRegistryPromptValueIsNotPersistedInSnapshotJSON(t *testing.T) {
registry, err := NewModuleRegistry([]ModuleDefinition{
{
ID: module.Metadata,
StanzaName: "metadata",
Builder: func(ModuleContext, any) (*module.Output, error) {
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: testRegistryValue{Message: "rich"}}, nil
},
PromptExporter: func(any) (any, error) {
return testRegistryValue{Message: "prompt-only"}, nil
},
},
})
if err != nil {
t.Fatalf("NewModuleRegistry() error = %v", err)
}
output, err := registry.BuildModule(testRegistryModuleContext(), module.ConfigItem{ID: module.Metadata})
if err != nil {
t.Fatalf("BuildModule() error = %v", err)
}
snapshot, err := module.NewSnapshot([]module.Output{*output})
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
data, err := json.Marshal(snapshot)
if err != nil {
t.Fatalf("Marshal() error = %v", err)
}
text := string(data)
if !strings.Contains(text, `"message":"rich"`) {
t.Fatalf("snapshot JSON missing rich value: %s", text)
}
if strings.Contains(text, "prompt-only") || strings.Contains(text, "promptValue") || strings.Contains(text, "PromptValue") {
t.Fatalf("snapshot JSON includes runtime-only prompt value: %s", text)
}
}
func TestHourlyDefaultModuleOptions(t *testing.T) {
definition := report.DefaultRegistry().MustLookup(report.Hourly)
var found bool
@@ -250,3 +422,15 @@ func TestSPCConvectiveOutlookDerivedRequirementAvailability(t *testing.T) {
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: struct{}{}}, nil
}
type testRegistryValue struct {
Message string `json:"message"`
}
func testRegistryModuleContext() ModuleContext {
return ModuleContext{
Resolved: report.Resolved{
Definition: report.DefaultRegistry().MustLookup(report.Daily),
},
}
}