package promptassets_test import ( "context" "encoding/json" "io/fs" "strings" "testing" "gitea.maximumdirect.net/eric/promptkit" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptassets" "gopkg.in/yaml.v3" ) type promptDefinition struct { ID string `yaml:"id"` Version string `yaml:"version"` DefaultProfile string `yaml:"default_profile"` Inputs []struct { Name string `yaml:"name"` Required bool `yaml:"required"` ContentType string `yaml:"content_type"` } `yaml:"inputs"` Output struct { Format string `yaml:"format"` ValidationMode string `yaml:"validation_mode"` SchemaPath string `yaml:"schema_path"` RepairAttempts *int `yaml:"repair_attempts"` } `yaml:"output"` } func TestPromptAssetsDeclareTheFourGeneratedTextPrompts(t *testing.T) { tests := []struct { path string id string schemaID string }{ {"daily/daily_generated_text.yml", "weather.daily_generated_text", "daily"}, {"today/today_generated_text.yml", "weather.today_generated_text", "today"}, {"tomorrow/tomorrow_generated_text.yml", "weather.tomorrow_generated_text", "tomorrow"}, {"hourly/hourly_generated_text.yml", "weather.hourly_generated_text", "hourly"}, } definitions := 0 if err := fs.WalkDir(promptassets.PromptFS(), ".", func(path string, entry fs.DirEntry, err error) error { if err != nil { return err } if !entry.IsDir() && strings.HasSuffix(path, ".yml") { definitions++ } return nil }); err != nil { t.Fatalf("walk embedded prompts: %v", err) } if definitions != len(tests) { t.Fatalf("prompt definitions = %d, want %d", definitions, len(tests)) } for _, tc := range tests { t.Run(tc.id, func(t *testing.T) { data, err := fs.ReadFile(promptassets.PromptFS(), tc.path) if err != nil { t.Fatalf("read prompt definition: %v", err) } var definition promptDefinition if err := yaml.Unmarshal(data, &definition); err != nil { t.Fatalf("decode prompt definition: %v", err) } if definition.ID != tc.id || definition.Version != "1.0.1" || definition.DefaultProfile != "gemini-flash-latest" { t.Fatalf("definition = %#v, want %s version 1.0.1 and gemini-flash-latest", definition, tc.id) } 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) } if definition.Output.Format != "json" || definition.Output.ValidationMode != "json_schema" || definition.Output.SchemaPath != tc.schemaID+".generated_text.schema.json" || definition.Output.RepairAttempts != nil { t.Fatalf("output = %#v, want JSON schema output without repair attempts", definition.Output) } if _, err := promptassets.Schema(tc.schemaID); err != nil { t.Fatalf("Schema(%q) error = %v", tc.schemaID, err) } }) } } func TestSchemasAreCanonicalAndIndependent(t *testing.T) { for _, id := range []string{"daily", "today", "tomorrow", "hourly"} { t.Run(id, func(t *testing.T) { data, err := promptassets.Schema(id) if err != nil { t.Fatalf("Schema() error = %v", err) } var schema struct { ID string `json:"$id"` Title string `json:"title"` Type string `json:"type"` AdditionalProperties bool `json:"additionalProperties"` Required []string `json:"required"` Properties map[string]any `json:"properties"` } if err := json.Unmarshal(data, &schema); err != nil { t.Fatalf("decode schema: %v", err) } if schema.Type != "object" || schema.AdditionalProperties || strings.Join(schema.Required, ",") != "summary,forecast_discussion,precipitation_timing" { t.Fatalf("schema = %#v, want strict generated-text object", schema) } if _, ok := schema.Properties["confidence"]; ok { t.Fatalf("schema properties = %#v, do not want retired confidence field", schema.Properties) } if id == "daily" && (schema.ID != "weatherreporter.daily.generated_text.schema.json" || schema.Title != "Daily GeneratedText") { t.Fatalf("daily schema identity = %q/%q, want corrected Daily identity", schema.ID, schema.Title) } data[0] = 'x' fresh, err := promptassets.Schema(id) if err != nil || fresh[0] != '{' { t.Fatalf("Schema() returned shared data or error: %v", err) } }) } } func TestPromptkitInspectsEmbeddedPromptsOffline(t *testing.T) { engine, err := promptkit.NewEngine(promptkit.Config{}, promptkit.WithPromptFS(promptassets.PromptFS(), "."), promptkit.WithSchemaFS(promptassets.SchemaFS(), "."), ) if err != nil { t.Fatalf("NewEngine() error = %v", err) } for _, id := range []string{"weather.daily_generated_text", "weather.today_generated_text", "weather.tomorrow_generated_text", "weather.hourly_generated_text"} { t.Run(id, func(t *testing.T) { inspection, err := engine.InspectPrompt(context.Background(), id, "1.0.1") if err != nil { t.Fatalf("InspectPrompt() error = %v", err) } if inspection.PromptID != id || inspection.PromptVersion != "1.0.1" || inspection.DefaultProfileID != "gemini-flash-latest" { t.Fatalf("inspection = %#v", inspection) } }) } } func TestPromptAssetsExcludeRetiredRuntimeSettings(t *testing.T) { if err := fs.WalkDir(promptassets.PromptFS(), ".", func(path string, entry fs.DirEntry, err error) error { if err != nil || entry.IsDir() { return err } data, err := fs.ReadFile(promptassets.PromptFS(), path) if err != nil { return err } for _, unwanted := range []string{"local-heavy", "pipeline-weather/", "application/json", "repair_attempts:", "weather.daily_report"} { if strings.Contains(string(data), unwanted) { t.Fatalf("%s contains retired runtime setting %q", path, unwanted) } } return nil }); err != nil { t.Fatalf("walk embedded prompts: %v", err) } }