CI / ci (push) Successful in 2m49s
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
489 lines
22 KiB
Go
489 lines
22 KiB
Go
package skills
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/harness"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/llm"
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
|
)
|
|
|
|
func date(y int, m time.Month, d int) store.LocalDate {
|
|
return store.LocalDateOf(time.Date(y, m, d, 0, 0, 0, 0, time.UTC))
|
|
}
|
|
|
|
// household asks for two Side quests, so four are requested.
|
|
var household = SideQuestsRequest{
|
|
Context: harness.HouseholdContext{
|
|
Today: date(2026, 10, 6),
|
|
Timezone: "America/New_York",
|
|
Regulars: []harness.RegularDinner{
|
|
{Name: "Beef Chili", Cuisine: "Tex-Mex", Method: "crockpot", Minutes: 240},
|
|
{Name: "Chicken Fajitas", Cuisine: "Tex-Mex", Method: "stovetop", Minutes: 30, Favorite: true},
|
|
{Name: "Spaghetti Bolognese", Cuisine: "Italian", Method: "stovetop", Minutes: 45},
|
|
},
|
|
QuestLog: []string{"Pad Thai"},
|
|
Passed: []string{"Chicken Tikka Masala"},
|
|
Plans: []harness.PlannedDinner{
|
|
{Date: date(2026, 10, 5), Name: "Chicken Fajitas"},
|
|
{Date: date(2026, 10, 6), Name: "Beef Chili"},
|
|
},
|
|
},
|
|
Needed: 2,
|
|
Pending: []string{"Shrimp Scampi"},
|
|
Declined: []string{"Chicken Tikka Masala", "Fish Tacos"},
|
|
}
|
|
|
|
// sideQuest renders one Side quest of a reply.
|
|
func sideQuest(name, reason string, cited ...string) string {
|
|
c, _ := json.Marshal(append([]string{}, cited...))
|
|
r, _ := json.Marshal(reason)
|
|
return fmt.Sprintf(`{"name":%q,"cuisine":"Fusion","method":"stovetop","est_minutes":35,"difficulty":"easy","reason":%s,"cited_regulars":%s,"search_query":%q}`,
|
|
name, r, c, strings.ToLower(name)+" recipe")
|
|
}
|
|
|
|
// batch renders a reply holding the Side quests given.
|
|
func batch(quests ...string) string {
|
|
return `{"suggestions":[` + strings.Join(quests, ",") + `]}`
|
|
}
|
|
|
|
var (
|
|
shawarma = sideQuest("Chicken Shawarma Bowls", "Spiced chicken and crisp toppings, like your Chicken Fajitas.", "Chicken Fajitas")
|
|
ragu = sideQuest("Beef Ragu Pappardelle", "A slow beef sauce like your Spaghetti Bolognese, on wide noodles.", "Spaghetti Bolognese")
|
|
verde = sideQuest("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "beef chili")
|
|
bulgogi = sideQuest("Beef Bulgogi", "Sweet, quick-seared beef for fans of Chicken Fajitas and Beef Chili.", "Chicken Fajitas", "Beef Chili")
|
|
)
|
|
|
|
// want returns the Side quest that sideQuest renders, with cited as the
|
|
// household writes them.
|
|
func want(name, reason string, cited ...string) SideQuest {
|
|
return SideQuest{
|
|
Name: name, Cuisine: "Fusion", Method: "stovetop", EstMinutes: 35, Difficulty: "easy",
|
|
Reason: reason, CitedRegulars: cited, SearchQuery: strings.ToLower(name) + " recipe",
|
|
}
|
|
}
|
|
|
|
var (
|
|
wantShawarma = want("Chicken Shawarma Bowls", "Spiced chicken and crisp toppings, like your Chicken Fajitas.", "Chicken Fajitas")
|
|
wantRagu = want("Beef Ragu Pappardelle", "A slow beef sauce like your Spaghetti Bolognese, on wide noodles.", "Spaghetti Bolognese")
|
|
wantVerde = want("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "Beef Chili")
|
|
wantBulgogi = want("Beef Bulgogi", "Sweet, quick-seared beef for fans of Chicken Fajitas and Beef Chili.", "Chicken Fajitas", "Beef Chili")
|
|
)
|
|
|
|
// suggest runs SuggestSideQuests on household with f and checks f was
|
|
// called exactly calls times, each at the household temperature.
|
|
func suggest(t *testing.T, f *llm.Fake, in SideQuestsRequest, calls int) (SideQuests, []llm.Request, error) {
|
|
t.Helper()
|
|
out, err := SuggestSideQuests(context.Background(), testConfig(t, f), "2026-W41", in)
|
|
reqs := f.Requests()
|
|
if len(reqs) != calls {
|
|
t.Fatalf("Generate called %d times, want %d", len(reqs), calls)
|
|
}
|
|
if g, _, _ := f.Remaining(); g != 0 {
|
|
t.Fatalf("%d scripted Generate results unused", g)
|
|
}
|
|
for i, req := range reqs {
|
|
if req.Temperature == nil || *req.Temperature != 0.7 {
|
|
t.Fatalf("request %d temperature = %v, want 0.7", i+1, req.Temperature)
|
|
}
|
|
if req.Schema == nil || req.Schema.Name != "suggest_side_quests" {
|
|
t.Fatalf("request %d schema = %v, want suggest_side_quests", i+1, req.Schema)
|
|
}
|
|
}
|
|
return out, reqs, err
|
|
}
|
|
|
|
func assertSideQuests(t *testing.T, got, want SideQuests) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("output = %+v\nwant %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsCleanBatch(t *testing.T) {
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(shawarma, ragu, verde, bulgogi)))
|
|
out, reqs, err := suggest(t, &f, household, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{Suggestions: []SideQuest{wantShawarma, wantRagu, wantVerde, wantBulgogi}})
|
|
|
|
system, user := reqs[0].System, reqs[0].Messages[0].Content
|
|
golden(t, "suggest_side_quests_clean.system.golden", system)
|
|
golden(t, "suggest_side_quests_clean.user.golden", user)
|
|
for _, part := range []string{
|
|
"## Household context",
|
|
"- Chicken Fajitas (Tex-Mex, stovetop, 30 min, favorite)",
|
|
"### Quest log\n- Pad Thai",
|
|
"### Passed, oldest first\n- Chicken Tikka Masala",
|
|
"- 2026-10-06 Tue: Beef Chili",
|
|
"### Pending Side quests\n- Shrimp Scampi",
|
|
"## Output contract",
|
|
`"cited_regulars"`,
|
|
`"search_query"`,
|
|
} {
|
|
if !strings.Contains(system, part) {
|
|
t.Errorf("system prompt lacks %q", part)
|
|
}
|
|
}
|
|
if strings.Index(system, "## Household context") > strings.Index(system, "## Output contract") {
|
|
t.Error("the output contract is not after the household context")
|
|
}
|
|
if user != "Suggest 4 Side quests." {
|
|
t.Errorf("user message = %q, want twice the 2 needed", user)
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsPersonaFirst(t *testing.T) {
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(shawarma)))
|
|
cfg := testConfig(t, &f)
|
|
cfg.Persona = "You are the household's cheerful kitchen helper."
|
|
if _, err := SuggestSideQuests(context.Background(), cfg, "2026-W41", household); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if system := f.Requests()[0].System; !strings.HasPrefix(system, cfg.Persona+"\n\n## Instructions\n") {
|
|
t.Fatalf("system prompt does not start with the persona then the instructions:\n%s", system)
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsInventedRegularDropped(t *testing.T) {
|
|
invented := sideQuest("Meatloaf Wellington", "Your Grandma's Meatloaf, wrapped in pastry.", "Grandma's Meatloaf")
|
|
partly := sideQuest("Fajita Meatballs", "Chicken Fajitas meets your Grandma's Meatloaf.", "Chicken Fajitas", "Grandma's Meatloaf")
|
|
none := sideQuest("Mushroom Risotto", "Creamy and comforting.")
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(invented, shawarma, partly, none)))
|
|
out, _, err := suggest(t, &f, household, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{wantShawarma},
|
|
Dropped: []DroppedSideQuest{
|
|
{Name: "Meatloaf Wellington", Why: `cites "Grandma's Meatloaf", which is not a Regular`},
|
|
{Name: "Fajita Meatballs", Why: `cites "Grandma's Meatloaf", which is not a Regular`},
|
|
{Name: "Mushroom Risotto", Why: "cites no Regular"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsSharedNormalizedRegularNotRewritten(t *testing.T) {
|
|
in := household
|
|
in.Context.Regulars = append(slices.Clone(household.Context.Regulars),
|
|
harness.RegularDinner{Name: "Easy Beef Chili", Cuisine: "Tex-Mex", Method: "stovetop", Minutes: 30})
|
|
plain := sideQuest("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "beef chili")
|
|
easy := sideQuest("Frito Pie", "Your Easy Beef Chili over corn chips.", "EASY beef chili")
|
|
either := sideQuest("Tamale Bake", "Your Beef Chili under a cornbread crust.", "Beef Chili!")
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(plain, easy, either)))
|
|
out, _, err := suggest(t, &f, in, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{
|
|
want("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "Beef Chili"),
|
|
want("Frito Pie", "Your Easy Beef Chili over corn chips.", "Easy Beef Chili"),
|
|
},
|
|
Dropped: []DroppedSideQuest{
|
|
{Name: "Tamale Bake", Why: `cites "Beef Chili!", which could be any of the Regulars ["Beef Chili" "Easy Beef Chili"]`},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsReasonNamesResolvedRegular(t *testing.T) {
|
|
in := household
|
|
in.Context.Regulars = append(slices.Clone(household.Context.Regulars),
|
|
harness.RegularDinner{Name: "Easy Beef Chili", Cuisine: "Tex-Mex", Method: "stovetop", Minutes: 30})
|
|
other := sideQuest("Chili Verde", "Your Beef Chili gone green, with pork and tomatillos.", "Easy Beef Chili")
|
|
easy := sideQuest("Frito Pie", "Your Easy Beef Chili over corn chips.", "Easy Beef Chili")
|
|
plain := sideQuest("Chili Mac", "The Beef Chili, not the Easy Beef Chili, over macaroni.", "Beef Chili")
|
|
inside := sideQuest("Tamale Bake", "Your Easy Beef Chili under a cornbread crust.", "Beef Chili")
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(other, easy, plain, inside)))
|
|
out, _, err := suggest(t, &f, in, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{
|
|
want("Frito Pie", "Your Easy Beef Chili over corn chips.", "Easy Beef Chili"),
|
|
want("Chili Mac", "The Beef Chili, not the Easy Beef Chili, over macaroni.", "Beef Chili"),
|
|
},
|
|
Dropped: []DroppedSideQuest{
|
|
{Name: "Chili Verde", Why: "reason names none of its cited Regulars"},
|
|
{Name: "Tamale Bake", Why: "reason names none of its cited Regulars"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsReasonMentionRule(t *testing.T) {
|
|
const none = "reason names none of its cited Regulars"
|
|
for name, tc := range map[string]struct {
|
|
extra []string // Regulars added to household's
|
|
reason string
|
|
cited string
|
|
why string // "" when the Side quest is kept
|
|
}{
|
|
"exact name": {nil, "Like your Beef Chili, gone green.", "Beef Chili", ""},
|
|
"other case and spacing": {nil, "Like your beef CHILI, gone green.", "Beef Chili", ""},
|
|
"sentence punctuation": {nil, `Green, like "Beef Chili" (yours).`, "Beef Chili", ""},
|
|
"possessive": {nil, "Beef Chili's spices, gone green.", "Beef Chili", ""},
|
|
"joined by punctuation": {nil, "A Beef Chili-inspired stew.", "Beef Chili", ""},
|
|
"no mention": {nil, "Grilled skewers with a peanut sauce.", "Beef Chili", none},
|
|
"letters run on": {[]string{"Ham"}, "Like your Hamburgers, glazed.", "Ham", none},
|
|
"stored punctuation written": {[]string{"Easy Beef Chili!"}, "Your Easy Beef Chili! over corn chips.", "Easy Beef Chili!", ""},
|
|
"stored punctuation as period": {[]string{"Easy Beef Chili!"}, "Corn chips under your Easy Beef Chili.", "Easy Beef Chili!", ""},
|
|
"inside stored punctuation": {[]string{"Easy Beef Chili!"}, "Your Easy Beef Chili! over corn chips.", "Beef Chili", none},
|
|
"inside before period": {[]string{"Easy Beef Chili!"}, "Corn chips under your Easy Beef Chili.", "Beef Chili", none},
|
|
"longer name cited": {[]string{"Easy Beef Chili"}, "Your Easy Beef Chili over corn chips.", "Easy Beef Chili", ""},
|
|
"shorter inside longer": {[]string{"Easy Beef Chili"}, "Your Easy Beef Chili over corn chips.", "Beef Chili", none},
|
|
"longer not mentioned": {[]string{"Easy Beef Chili"}, "Your Beef Chili over corn chips.", "Easy Beef Chili", none},
|
|
"shorter also on its own": {[]string{"Easy Beef Chili"}, "The Beef Chili, not the Easy Beef Chili.", "Beef Chili", ""},
|
|
"punctuation-only difference": {[]string{"Beef Chili!"}, "Your Beef Chili! over corn chips.", "Beef Chili!", ""},
|
|
"bare name inside punctuated": {[]string{"Beef Chili!"}, "Your Beef Chili! over corn chips.", "Beef Chili", none},
|
|
"spelled alike by both": {[]string{"Beef Chili!"}, "Corn chips under your Beef Chili.", "Beef Chili", none},
|
|
"punctuation then a letter": {[]string{"Beef Chili!"}, "Your Beef Chili!X is our inspiration.", "Beef Chili!", none},
|
|
"letter before punctuation": {[]string{"¡Beef Chili"}, "Your X¡Beef Chili is our inspiration.", "¡Beef Chili", none},
|
|
"spacing before punctuation": {[]string{"Beef Chili!"}, "Your Beef Chili ! over corn chips.", "Beef Chili!", ""},
|
|
"spacing after punctuation": {[]string{"¡Beef Chili"}, "Your ¡ Beef Chili is our inspiration.", "¡Beef Chili", ""},
|
|
"stored spacing left off": {[]string{"Beef Chili !", "Chili!"}, "Like your Beef Chili!", "Beef Chili !", ""},
|
|
"inside unspaced punctuation": {[]string{"Beef Chili !", "Chili!"}, "Like your Beef Chili!", "Chili!", none},
|
|
"stored punctuation partly": {[]string{"Beef Chili!!", "Chili!"}, "Like your Beef Chili!", "Beef Chili!!", ""},
|
|
"inside partial punctuation": {[]string{"Beef Chili!!", "Chili!"}, "Like your Beef Chili!", "Chili!", none},
|
|
"partly then a letter": {[]string{"Beef Chili!!", "Chili!"}, "Your Beef Chili!!X is our inspiration.", "Chili!", none},
|
|
"leading punctuation partly": {[]string{"¡¡Beef Chili", "¡Beef"}, "Like your ¡Beef Chili.", "¡¡Beef Chili", ""},
|
|
"inside partial leading": {[]string{"¡¡Beef Chili", "¡Beef"}, "Like your ¡Beef Chili.", "¡Beef", none},
|
|
"stored symbol left off": {[]string{"Green Chili 🌶"}, "Like your Green Chili, gone green.", "Green Chili 🌶", none},
|
|
"stored accent left off": {[]string{"Chicken Cafe\u0301"}, "Like your Chicken Cafe, gone green.", "Chicken Cafe\u0301", none},
|
|
"accent written after name": {[]string{"Chicken Cafe"}, "Like your Chicken Cafe\u0301, gone green.", "Chicken Cafe", none},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
in := household
|
|
in.Context.Regulars = slices.Clone(household.Context.Regulars)
|
|
for _, r := range tc.extra {
|
|
in.Context.Regulars = append(in.Context.Regulars, harness.RegularDinner{Name: r, Cuisine: "Tex-Mex", Method: "stovetop", Minutes: 30})
|
|
}
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(sideQuest("Frito Pie", tc.reason, tc.cited))))
|
|
out, _, err := suggest(t, &f, in, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := SideQuests{Suggestions: []SideQuest{want("Frito Pie", oneLine(tc.reason), tc.cited)}}
|
|
if tc.why != "" {
|
|
result = SideQuests{Suggestions: []SideQuest{}, Dropped: []DroppedSideQuest{{Name: "Frito Pie", Why: tc.why}}}
|
|
}
|
|
assertSideQuests(t, out, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsDuplicatesDropped(t *testing.T) {
|
|
declined := sideQuest("The Best Homemade Chicken Tikka-Masala", "Spiced like your Chicken Fajitas.", "Chicken Fajitas")
|
|
oldDeclined := sideQuest("Fish Tacos!", "Tortillas like your Chicken Fajitas.", "Chicken Fajitas")
|
|
regular := sideQuest("Easy Beef Chili", "Just like your Beef Chili.", "Beef Chili")
|
|
questLog := sideQuest("Pad Thai", "Noodles like your Spaghetti Bolognese.", "Spaghetti Bolognese")
|
|
pending := sideQuest("Shrimp Scampi", "Pasta like your Spaghetti Bolognese.", "Spaghetti Bolognese")
|
|
again := sideQuest("Chicken Shawarma Bowls", "Chicken Fajitas, but Middle Eastern.", "Chicken Fajitas")
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(declined, oldDeclined, regular, questLog, pending, shawarma, again)))
|
|
in := household
|
|
in.Needed = 4
|
|
out, _, err := suggest(t, &f, in, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
known := "repeats a Regular, Quest log dinner, pending or passed Side quest"
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{wantShawarma},
|
|
Dropped: []DroppedSideQuest{
|
|
{Name: "The Best Homemade Chicken Tikka-Masala", Why: known},
|
|
{Name: "Fish Tacos!", Why: known},
|
|
{Name: "Easy Beef Chili", Why: known},
|
|
{Name: "Pad Thai", Why: known},
|
|
{Name: "Shrimp Scampi", Why: known},
|
|
{Name: "Chicken Shawarma Bowls", Why: `repeats the Side quest "Chicken Shawarma Bowls" in the same batch`},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsReasonRulesDropped(t *testing.T) {
|
|
long := sideQuest("Beef Stroganoff", "Your Beef Chili"+strings.Repeat(" and more", 20)+".", "Beef Chili")
|
|
unnamed := sideQuest("Chicken Satay", "Grilled skewers with a peanut sauce.", "Chicken Fajitas")
|
|
wrongOne := sideQuest("Beef Tacos", "Like your Beef Chili, in a shell.", "Chicken Fajitas")
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(long, unnamed, ragu, wrongOne)))
|
|
out, _, err := suggest(t, &f, household, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{wantRagu},
|
|
Dropped: []DroppedSideQuest{
|
|
{Name: "Beef Stroganoff", Why: "reason has 196 characters, over 180"},
|
|
{Name: "Chicken Satay", Why: "reason names none of its cited Regulars"},
|
|
{Name: "Beef Tacos", Why: "reason names none of its cited Regulars"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsAllDroppedIsNotRepaired(t *testing.T) {
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(batch(sideQuest("Pad Thai", "Like your Beef Chili.", "Beef Chili"))))
|
|
out, _, err := suggest(t, &f, household, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{
|
|
Suggestions: []SideQuest{},
|
|
Dropped: []DroppedSideQuest{{Name: "Pad Thai", Why: "repeats a Regular, Quest log dinner, pending or passed Side quest"}},
|
|
})
|
|
}
|
|
|
|
func TestSuggestSideQuestsMalformedRepairedOnce(t *testing.T) {
|
|
for name, tc := range map[string]struct{ first, want string }{
|
|
"truncated JSON": {
|
|
`{"suggestions":[` + shawarma + `,{"name":"Beef`,
|
|
"reply does not decode: unexpected EOF",
|
|
},
|
|
"omitted field": {
|
|
batch(strings.Replace(shawarma, `"cuisine":"Fusion",`, ``, 1)),
|
|
`suggestion has no "cuisine" field; give every field the schema requires`,
|
|
},
|
|
"unknown field": {
|
|
batch(strings.Replace(shawarma, `"easy",`, `"easy","spice":"mild",`, 1)),
|
|
`suggestion has unknown field "spice"`,
|
|
},
|
|
"bad method": {
|
|
batch(strings.Replace(shawarma, `"stovetop"`, `"air fryer"`, 1)),
|
|
`suggestion 1 method "air fryer" is not one of stovetop, oven, crockpot, grill, other`,
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(tc.first), reply(batch(shawarma, ragu)))
|
|
out, reqs, err := suggest(t, &f, household, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{Suggestions: []SideQuest{wantShawarma, wantRagu}})
|
|
repair := reqs[1].Messages
|
|
if len(repair) != 3 || repair[1].Content != tc.first {
|
|
t.Fatalf("repair turn does not quote the first reply: %+v", repair)
|
|
}
|
|
if !strings.Contains(repair[2].Content, tc.want) {
|
|
t.Fatalf("repair message = %q, want it to contain %q", repair[2].Content, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsFillerNameRepaired(t *testing.T) {
|
|
filler := sideQuest("The.", "Like your Beef Chili.", "Beef Chili")
|
|
first := batch(filler, filler)
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(first), reply(batch(shawarma, ragu)))
|
|
out, reqs, err := suggest(t, &f, household, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertSideQuests(t, out, SideQuests{Suggestions: []SideQuest{wantShawarma, wantRagu}})
|
|
repair := reqs[1].Messages
|
|
if len(repair) != 3 || repair[1].Content != first {
|
|
t.Fatalf("repair turn does not quote the first reply: %+v", repair)
|
|
}
|
|
for _, want := range []string{
|
|
`suggestion 1 name "The." has only filler words; name the dish`,
|
|
`suggestion 2 name "The." has only filler words; name the dish`,
|
|
} {
|
|
if !strings.Contains(repair[2].Content, want) {
|
|
t.Fatalf("repair message = %q, want it to contain %q", repair[2].Content, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSuggestSideQuestsMalformedTwiceFails(t *testing.T) {
|
|
var f llm.Fake
|
|
f.QueueGenerate(reply(`{"suggestions":`), reply(`{"suggestions":[]}`))
|
|
out, _, err := suggest(t, &f, household, 2)
|
|
var bad *harness.BadOutputError
|
|
if !errors.As(err, &bad) || !errors.Is(err, llm.ErrBadOutput) {
|
|
t.Fatalf("err = %v, want a BadOutputError", err)
|
|
}
|
|
if !slices.Equal(bad.Repair, []string{"suggestions is empty; give 4 Side quests"}) {
|
|
t.Fatalf("repair problems = %q", bad.Repair)
|
|
}
|
|
if out.Suggestions != nil {
|
|
t.Fatalf("output = %+v, want none", out)
|
|
}
|
|
}
|
|
|
|
func TestSideQuestsValidate(t *testing.T) {
|
|
s := suggestSideQuests{in: household}
|
|
if p := s.Validate(SideQuests{Suggestions: []SideQuest{wantShawarma}}); len(p) != 0 {
|
|
t.Fatalf("valid output has problems: %q", p)
|
|
}
|
|
if p := s.Validate(SideQuests{Suggestions: []SideQuest{}, Dropped: []DroppedSideQuest{{Name: "x", Why: "y"}}}); len(p) != 0 {
|
|
t.Fatalf("output whose Side quests were all dropped has problems: %q", p)
|
|
}
|
|
for name, tc := range map[string]struct {
|
|
out SideQuests
|
|
want []string
|
|
}{
|
|
"empty": {SideQuests{}, []string{"suggestions is empty; give 4 Side quests"}},
|
|
"too many": {
|
|
SideQuests{Suggestions: []SideQuest{wantShawarma, wantRagu, wantVerde, wantBulgogi, wantShawarma}},
|
|
[]string{"suggestions has 5 entries; give at most 4 Side quests"},
|
|
},
|
|
"malformed": {
|
|
SideQuests{Suggestions: []SideQuest{{Name: " ", Method: "air fryer", EstMinutes: 0, Difficulty: "trivial", CitedRegulars: []string{}}}},
|
|
[]string{
|
|
"suggestion 1 has an empty name; give one",
|
|
"suggestion 1 has an empty cuisine; give one",
|
|
"suggestion 1 has an empty reason; give one",
|
|
"suggestion 1 has an empty search_query; give one",
|
|
`suggestion 1 method "air fryer" is not one of stovetop, oven, crockpot, grill, other`,
|
|
"suggestion 1 est_minutes 0 is not positive; give the minutes from prep to table",
|
|
`suggestion 1 difficulty "trivial" is not one of easy, medium, hard`,
|
|
},
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := s.Validate(tc.out); !slices.Equal(got, tc.want) {
|
|
t.Fatalf("problems =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(tc.want, "\n"))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSideQuestsPromptInputRejects(t *testing.T) {
|
|
noRegulars := household
|
|
noRegulars.Context.Regulars = nil
|
|
none := household
|
|
none.Needed = 0
|
|
for name, tc := range map[string]struct {
|
|
in SideQuestsRequest
|
|
want string
|
|
}{
|
|
"no Regulars": {noRegulars, "household has no Regulars for Side quests to be based on"},
|
|
"none needed": {none, "0 Side quests needed; the number must be positive"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
_, err := suggestSideQuests{in: tc.in}.PromptInput(tc.in)
|
|
if err == nil || err.Error() != tc.want {
|
|
t.Fatalf("err = %v, want %q", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|