From 92fcbfcc050a189854acde00c7f4df9fcaa93e9c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 15 Jun 2026 20:25:12 +0000 Subject: [PATCH] Attach prompt export values in module registry --- internal/briefing/modules.go | 12 ++ internal/briefing/modules_test.go | 184 ++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/internal/briefing/modules.go b/internal/briefing/modules.go index c7e7a36..5a4a5ba 100644 --- a/internal/briefing/modules.go +++ b/internal/briefing/modules.go @@ -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 } diff --git a/internal/briefing/modules_test.go b/internal/briefing/modules_test.go index 941b683..46f199b 100644 --- a/internal/briefing/modules_test.go +++ b/internal/briefing/modules_test.go @@ -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), + }, + } +}