Correct curated prompt package contracts
This commit is contained in:
@@ -4,11 +4,17 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -16,7 +22,10 @@ type promptDefinition struct {
|
||||
ID string `yaml:"id"`
|
||||
Version string `yaml:"version"`
|
||||
DefaultProfile string `yaml:"default_profile"`
|
||||
Inputs []struct {
|
||||
Messages []struct {
|
||||
ContentFile string `yaml:"content_file"`
|
||||
} `yaml:"messages"`
|
||||
Inputs []struct {
|
||||
Name string `yaml:"name"`
|
||||
Required bool `yaml:"required"`
|
||||
ContentType string `yaml:"content_type"`
|
||||
@@ -71,6 +80,15 @@ func TestPromptAssetsDeclareTheFourGeneratedTextPrompts(t *testing.T) {
|
||||
if definition.ID != tc.id || definition.Version != "2.0.0" || definition.DefaultProfile != tc.profile {
|
||||
t.Fatalf("definition = %#v, want %s version 2.0.0 and profile %s", definition, tc.id, tc.profile)
|
||||
}
|
||||
sharedInstruction := false
|
||||
for _, message := range definition.Messages {
|
||||
if message.ContentFile == "../common/data_package.user.md" {
|
||||
sharedInstruction = true
|
||||
}
|
||||
}
|
||||
if !sharedInstruction {
|
||||
t.Fatalf("definition messages = %#v, want shared data-package instruction", definition.Messages)
|
||||
}
|
||||
if len(definition.Inputs) != 1 || definition.Inputs[0].Name != "data_package" || !definition.Inputs[0].Required || definition.Inputs[0].ContentType != "application/yaml" {
|
||||
t.Fatalf("inputs = %#v, want one required YAML data_package", definition.Inputs)
|
||||
}
|
||||
@@ -84,6 +102,105 @@ func TestPromptAssetsDeclareTheFourGeneratedTextPrompts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedPromptReferencesSerializedPathsAndPreservesHazardLocality(t *testing.T) {
|
||||
sharedPrompt, err := fs.ReadFile(promptassets.PromptFS(), "common/data_package.user.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read shared prompt: %v", err)
|
||||
}
|
||||
pkgYAML := representativeDataPackageYAML(t)
|
||||
paths := regexp.MustCompile(`briefing(?:\.[a-z_]+)+`).FindAllString(string(sharedPrompt), -1)
|
||||
if len(paths) == 0 {
|
||||
t.Fatal("shared prompt does not reference briefing paths")
|
||||
}
|
||||
for _, path := range paths {
|
||||
if !yamlPathExists(t, pkgYAML, path) {
|
||||
t.Fatalf("shared prompt references path %q that is absent from representative data package:\n%s", path, pkgYAML)
|
||||
}
|
||||
}
|
||||
for _, obsoletePath := range []string{
|
||||
"briefing.metadata.alerts",
|
||||
"briefing.derived_daily_summary",
|
||||
"briefing.derived_daypart_summaries",
|
||||
"briefing.precip_timing",
|
||||
"briefing.outdoor_windows",
|
||||
} {
|
||||
if strings.Contains(string(sharedPrompt), obsoletePath) {
|
||||
t.Fatalf("shared prompt references obsolete path %q", obsoletePath)
|
||||
}
|
||||
}
|
||||
for _, requiredGuidance := range []string{
|
||||
"location-matched local conclusions",
|
||||
"regional context unless their own geography establishes point relevance",
|
||||
"never present them as point-local hazards solely because they are included",
|
||||
} {
|
||||
if !strings.Contains(string(sharedPrompt), requiredGuidance) {
|
||||
t.Fatalf("shared prompt is missing hazard-locality guidance %q", requiredGuidance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func representativeDataPackageYAML(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
generatedAt := time.Date(2026, 5, 29, 10, 0, 0, 0, time.UTC)
|
||||
snapshot, err := module.NewSnapshot([]module.Output{
|
||||
{ID: module.Metadata, StanzaName: "metadata", Value: map[string]any{"location": "Testville"}},
|
||||
{ID: module.AlertDigest, StanzaName: "alert_digest", Value: map[string]any{"relevant_count": 1}},
|
||||
{ID: module.SPCConvectiveOutlooks, StanzaName: "spc_convective_outlooks", Value: map[string]any{"risk_digest": []any{}, "outlooks": []any{}}},
|
||||
{ID: module.DerivedDailySummary, StanzaName: "derived_daily_summary", Value: map[string]any{"theme": "dry"}},
|
||||
{ID: module.DerivedDaypartSummaries, StanzaName: "derived_daypart_summaries", Value: map[string]any{"dayparts": []any{}}},
|
||||
{ID: module.PrecipTiming, StanzaName: "precip_timing", Value: map[string]any{"maximum_probability": 0}},
|
||||
{ID: module.OutdoorWindows, StanzaName: "outdoor_windows", Value: map[string]any{"windows": []any{}}},
|
||||
{ID: module.NarrativeForecast, StanzaName: "narrative_forecast", Value: map[string]any{"periods": []any{}}},
|
||||
{ID: module.AreaForecastDiscussion, StanzaName: "area_forecast_discussion", Value: map[string]any{"key_messages": []any{}, "short_term": "", "long_term": ""}},
|
||||
{ID: module.SPCConvectiveDiscussion, StanzaName: "spc_convective_discussion", Value: map[string]any{"discussions": []any{}}},
|
||||
{ID: module.WeatherStory, StanzaName: "weather_story", Value: map[string]any{}},
|
||||
{ID: module.CurrentConditions, StanzaName: "current_conditions", Value: map[string]any{"temperature": 72}},
|
||||
{ID: module.HourlyForecast, StanzaName: "hourly_forecast", Value: map[string]any{"periods": []any{}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewSnapshot() error = %v", err)
|
||||
}
|
||||
pkg, err := promptinput.Build(promptinput.BuildRequest{
|
||||
Metadata: promptinput.Metadata{
|
||||
RunID: "20260529T100000Z_daily",
|
||||
ReportID: report.Daily,
|
||||
PromptID: "weather.daily_generated_text",
|
||||
GeneratedAt: generatedAt,
|
||||
Timezone: "America/Chicago",
|
||||
ValidPeriod: timeutil.Period{Start: generatedAt, End: generatedAt.Add(24 * time.Hour)},
|
||||
},
|
||||
Modules: snapshot,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
data, err := promptinput.MarshalYAML(pkg)
|
||||
if err != nil {
|
||||
t.Fatalf("MarshalYAML() error = %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func yamlPathExists(t *testing.T, data []byte, path string) bool {
|
||||
t.Helper()
|
||||
var document map[string]any
|
||||
if err := yaml.Unmarshal(data, &document); err != nil {
|
||||
t.Fatalf("decode representative data package: %v", err)
|
||||
}
|
||||
var current any = document
|
||||
for _, segment := range strings.Split(path, ".") {
|
||||
mapping, ok := current.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
current, ok = mapping[segment]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestSchemasAreCanonicalAndIndependent(t *testing.T) {
|
||||
for _, id := range []string{"daily", "today", "tomorrow", "hourly"} {
|
||||
t.Run(id, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user