Attach prompt export values in module registry
This commit is contained in:
@@ -21,6 +21,8 @@ type ModuleContext struct {
|
|||||||
|
|
||||||
type ModuleBuilder func(ModuleContext, any) (*module.Output, error)
|
type ModuleBuilder func(ModuleContext, any) (*module.Output, error)
|
||||||
|
|
||||||
|
type ModulePromptExporter func(value any) (any, error)
|
||||||
|
|
||||||
type ModuleDefinition struct {
|
type ModuleDefinition struct {
|
||||||
ID module.ID
|
ID module.ID
|
||||||
StanzaName string
|
StanzaName string
|
||||||
@@ -31,6 +33,7 @@ type ModuleDefinition struct {
|
|||||||
MissingData module.MissingDataBehavior
|
MissingData module.MissingDataBehavior
|
||||||
AllowDuplicate bool
|
AllowDuplicate bool
|
||||||
Builder ModuleBuilder
|
Builder ModuleBuilder
|
||||||
|
PromptExporter ModulePromptExporter
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleRegistry struct {
|
type ModuleRegistry struct {
|
||||||
@@ -130,6 +133,15 @@ func (r ModuleRegistry) BuildModule(ctx ModuleContext, item module.ConfigItem) (
|
|||||||
if output.StanzaName != definition.StanzaName {
|
if output.StanzaName != definition.StanzaName {
|
||||||
return nil, fmt.Errorf("module %q produced stanza %q, want %q", definition.ID, 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
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package briefing
|
package briefing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -40,6 +42,9 @@ func TestDefaultReportModulesBuildSnapshots(t *testing.T) {
|
|||||||
t.Fatalf("BuildModule(%s) error = %v", item.ID, err)
|
t.Fatalf("BuildModule(%s) error = %v", item.ID, err)
|
||||||
}
|
}
|
||||||
if output != nil {
|
if output != nil {
|
||||||
|
if output.DataPackageValue() == nil {
|
||||||
|
t.Fatalf("BuildModule(%s) data package value = nil", item.ID)
|
||||||
|
}
|
||||||
outputs = append(outputs, *output)
|
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) {
|
func TestHourlyDefaultModuleOptions(t *testing.T) {
|
||||||
definition := report.DefaultRegistry().MustLookup(report.Hourly)
|
definition := report.DefaultRegistry().MustLookup(report.Hourly)
|
||||||
var found bool
|
var found bool
|
||||||
@@ -250,3 +422,15 @@ func TestSPCConvectiveOutlookDerivedRequirementAvailability(t *testing.T) {
|
|||||||
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
|
func noopModuleBuilder(ModuleContext, any) (*module.Output, error) {
|
||||||
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: struct{}{}}, nil
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user