Files
MealQuest/internal/ai/skills/archive_title_test.go
T
dev_agentandClaude Opus 5.5 fdfd0123fb
CI / ci (push) Successful in 4m48s
CI / ci (pull_request) Successful in 4m53s
Add the archive_title skill and title Supply runs on generation
archive_title v1 writes a Supply run's post title for the Supply archive
from the week's dinner names in day order. Validation requires 1-48
characters on one line, no emoji and no surrounding quotation marks; a
reply breaking a rule gets the harness's one repair turn.

shopping.Generate and GeneratePushed run it, outside the generation's
transaction, whenever the stored list is still untitled, and store
shopping_lists.title only while the list is untitled and not frozen. A
title failure never fails the generation: the list stays untitled, the
failure is logged and recorded in ai_runs, and the week's next
generation tries again. A titled list keeps its title on remakes.

Maestro task fa4b94c9-6623-41ab-8d2a-0ca7d1e6401a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:31:59 -04:00

171 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package skills
import (
"context"
"errors"
"fmt"
"slices"
"strings"
"testing"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/harness"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/llm"
)
// titleWeek is a week of seven dinners, Monday first.
var titleWeek = ArchiveTitleRequest{Dinners: []string{
"Beef Chili", "Pot Roast", "Chicken Fajitas", "Spaghetti Bolognese", "Sheet Pan Salmon", "Pad Thai", "Fish Tacos",
}}
func titleReply(title string) llm.GenerateResult {
return reply(fmt.Sprintf(`{"title":%q}`, title))
}
// title runs TitleArchive on in with f and checks f was called exactly
// calls times, each at the household temperature with the archive_title
// schema.
func title(t *testing.T, f *llm.Fake, in ArchiveTitleRequest, calls int) (ArchiveTitle, []llm.Request, error) {
t.Helper()
out, err := TitleArchive(context.Background(), testConfig(t, f), "2026-10-12", 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 != "archive_title" {
t.Fatalf("request %d schema = %v, want archive_title", i+1, req.Schema)
}
}
return out, reqs, err
}
func TestArchiveTitleValid(t *testing.T) {
var f llm.Fake
f.QueueGenerate(titleReply(" Slow Simmers\nand Taco Night "))
out, reqs, err := title(t, &f, titleWeek, 1)
if err != nil {
t.Fatal(err)
}
if out.Title != "Slow Simmers and Taco Night" {
t.Fatalf("title = %q", out.Title)
}
system, user := reqs[0].System, reqs[0].Messages[0].Content
golden(t, "archive_title_valid.system.golden", system)
golden(t, "archive_title_valid.user.golden", user)
want := "Title the week whose dinners are, in day order from Monday:\n" +
"- Beef Chili\n- Pot Roast\n- Chicken Fajitas\n- Spaghetti Bolognese\n- Sheet Pan Salmon\n- Pad Thai\n- Fish Tacos"
if user != want {
t.Errorf("user message = %q, want %q", user, want)
}
if !strings.Contains(system, "## Output contract") || !strings.Contains(system, `"title"`) {
t.Error("system prompt lacks the output contract")
}
}
// titleRepaired checks a first reply is repaired once, quoting it and the
// problem wanted.
func titleRepaired(t *testing.T, first, want string) {
t.Helper()
var f llm.Fake
f.QueueGenerate(titleReply(first), titleReply("Chili, Roast and Taco Night"))
out, reqs, err := title(t, &f, titleWeek, 2)
if err != nil {
t.Fatal(err)
}
if out.Title != "Chili, Roast and Taco Night" {
t.Fatalf("title = %q", out.Title)
}
repair := reqs[1].Messages
if len(repair) != 3 || repair[1].Content != fmt.Sprintf(`{"title":%q}`, first) {
t.Fatalf("repair turn does not quote the first reply: %+v", repair)
}
if !strings.Contains(repair[2].Content, want) {
t.Fatalf("repair message = %q, want it to contain %q", repair[2].Content, want)
}
}
func TestArchiveTitleTooLongRepaired(t *testing.T) {
long := "A Week of Slow Simmers, Sizzling Fajitas and Taco Night" // 55 characters
titleRepaired(t, long, fmt.Sprintf("title %q has 55 characters; shorten it to at most 48", long))
}
func TestArchiveTitleEmojiRepaired(t *testing.T) {
titleRepaired(t, "Taco Night 🌮", `title "Taco Night 🌮" has an emoji (U+1F32E); use words only`)
}
func TestArchiveTitleQuotedRepaired(t *testing.T) {
titleRepaired(t, `"Taco Night"`, `title "\"Taco Night\"" starts with a quotation mark; give the title without quotation marks around it`)
}
func TestArchiveTitleEmojiTwiceFails(t *testing.T) {
var f llm.Fake
f.QueueGenerate(titleReply("Chili Week 🔥"), titleReply("Chili Week ❤️"))
out, _, err := title(t, &f, titleWeek, 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{`title "Chili Week ❤️" has an emoji (U+2764); use words only`}) {
t.Fatalf("repair problems = %q", bad.Repair)
}
if out.Title != "" {
t.Fatalf("title = %q, want none", out.Title)
}
}
func TestArchiveTitleRules(t *testing.T) {
s := archiveTitle{}
for _, tc := range []struct {
title string
want []string
}{
{"Taco Night", nil},
{strings.Repeat("a", TitleLimit), nil},
{"Crème Brûlée & Co.", nil},
{"Fajitas — Then Tacos", nil},
{"Mom's Pot Roast Week", nil},
{" Spaced out ", nil},
{"", []string{"title is empty; give a title of 1 to 48 characters"}},
{" \n ", []string{"title is empty; give a title of 1 to 48 characters"}},
{strings.Repeat("é", TitleLimit+1), []string{fmt.Sprintf("title %q has 49 characters; shorten it to at most 48", strings.Repeat("é", TitleLimit+1))}},
{"Hot ☕ Week", []string{`title "Hot ☕ Week" has an emoji (U+2615); use words only`}},
{"Star ⭐ Week", []string{`title "Star ⭐ Week" has an emoji (U+2B50); use words only`}},
{"Go 🇮🇹", []string{`title "Go 🇮🇹" has an emoji (U+1F1EE); use words only`}},
{"‘Taco Night’", []string{`title "‘Taco Night’" starts with a quotation mark; give the title without quotation marks around it`}},
{"Taco Night”", []string{`title "Taco Night”" ends with a quotation mark; give the title without quotation marks around it`}},
{"«Pasta» 🍝 " + strings.Repeat("x", 45), []string{
fmt.Sprintf("title %q has 55 characters; shorten it to at most 48", "«Pasta» 🍝 "+strings.Repeat("x", 45)),
fmt.Sprintf("title %q has an emoji (U+1F35D); use words only", "«Pasta» 🍝 "+strings.Repeat("x", 45)),
fmt.Sprintf("title %q starts with a quotation mark; give the title without quotation marks around it", "«Pasta» 🍝 "+strings.Repeat("x", 45)),
}},
} {
if got := s.Validate(ArchiveTitle{Title: tc.title}); !slices.Equal(got, tc.want) {
t.Errorf("Validate(%q) =\n%q\nwant\n%q", tc.title, got, tc.want)
}
}
}
func TestArchiveTitleBadRequestFailsBeforeCall(t *testing.T) {
for name, tc := range map[string]struct {
in ArchiveTitleRequest
want string
}{
"no dinners": {ArchiveTitleRequest{}, "the week has no dinners to title it from"},
"blank dinner": {ArchiveTitleRequest{Dinners: []string{"Pad Thai", " \n"}}, "dinner 2 has a blank name"},
} {
t.Run(name, func(t *testing.T) {
var f llm.Fake
_, _, err := title(t, &f, tc.in, 0)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("err = %v, want one containing %q", err, tc.want)
}
})
}
}