Make recipe repairs conditional and keep pasted links out of logs
F1: A candidate or link repair now writes the dinner only if it is still unarchived and as observed before grounding (normalized_name, grounding_status, grounding_revision and recipe_id). A dinner renamed or grounded some other way meanwhile is ErrGroundConflict (409 ground_conflict); one archived, before or during the repair, is ErrArchived (409 dinner_archived). Manual repairs reject archived dinners too. F2: Any failure grounding a candidate or link by hand wraps ErrGroundFailed, which the API logs as reason=ground_failed with the dinner id only, never the pasted link or dependency error text. GroundURL no longer formats the link into its error. Maestro task 8741cedb-1849-4e3f-8d05-f5648c77c7ee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1106,3 +1106,103 @@ func TestGroundTimesOut(t *testing.T) {
|
||||
t.Errorf("grounding_status = %s, want failed as before", row.GroundingStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFixInFlightWhenDinnerChanges: a link still grounding when its dinner
|
||||
// is renamed, archived or grounded some other way writes nothing; the
|
||||
// change made meanwhile is kept.
|
||||
func TestFixInFlightWhenDinnerChanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
change func(t *testing.T, e *env, id string)
|
||||
want error
|
||||
// kept checks the dinner shows the change, not the link's recipe.
|
||||
kept func(row db.Dinner) bool
|
||||
}{
|
||||
{"renamed", func(t *testing.T, e *env, id string) {
|
||||
if _, err := e.svc.Update(t.Context(), id, dinners.Patch{Name: ptr("Chicken curry")}, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}, dinners.ErrGroundConflict, func(row db.Dinner) bool {
|
||||
return row.Name == "Chicken curry" && row.GroundingStatus == dinners.GroundingFailed && !row.RecipeID.Valid
|
||||
}},
|
||||
{"archived", func(t *testing.T, e *env, id string) {
|
||||
if _, err := e.svc.Archive(t.Context(), id, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}, dinners.ErrArchived, func(row db.Dinner) bool {
|
||||
return row.Status == dinners.StatusArchived && row.GroundingStatus == dinners.GroundingFailed && !row.RecipeID.Valid
|
||||
}},
|
||||
{"grounded by hand", func(t *testing.T, e *env, id string) {
|
||||
if _, err := e.svc.Ground(t.Context(), id, dinners.Fix{Manual: &dinners.ManualRecipe{
|
||||
Ingredients: []string{"1 lb beef"}, Minutes: ptr(240), Method: ptr("crockpot"),
|
||||
}}, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}, dinners.ErrGroundConflict, func(row db.Dinner) bool {
|
||||
return row.GroundingStatus == dinners.GroundingManual && row.Method.String == "crockpot"
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newEnv(t)
|
||||
id := failedDinner(t, e, "Grandma's stew")
|
||||
e.model.QueueGenerate(recipestest.NormalizedReply("oven", 150))
|
||||
e.pages.Gate = make(chan struct{})
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := e.svc.Ground(context.Background(), id, dinners.Fix{RecipeURL: ptr(stew)}, member)
|
||||
done <- err
|
||||
}()
|
||||
e.waitFetched(t, stew)
|
||||
tt.change(t, e, id)
|
||||
close(e.pages.Gate)
|
||||
|
||||
if err := <-done; !errors.Is(err, tt.want) {
|
||||
t.Fatalf("err = %v, want %v", err, tt.want)
|
||||
}
|
||||
if row := e.dinner(t, id); !tt.kept(row) {
|
||||
t.Errorf("dinner = %+v, want the %s change kept and the link's recipe not written", row, tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFixArchivedDinner: an archived dinner cannot be grounded by hand.
|
||||
func TestFixArchivedDinner(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newEnv(t)
|
||||
id := failedDinner(t, e, "Grandma's stew")
|
||||
if _, err := e.svc.Archive(t.Context(), id, member); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := e.dinner(t, id)
|
||||
for _, f := range []dinners.Fix{
|
||||
{RecipeURL: ptr(stew)},
|
||||
{Manual: &dinners.ManualRecipe{Ingredients: []string{"1 lb beef"}, Minutes: ptr(240), Method: ptr("crockpot")}},
|
||||
} {
|
||||
if _, err := e.svc.Ground(t.Context(), id, f, member); !errors.Is(err, dinners.ErrArchived) {
|
||||
t.Errorf("fix %+v: err = %v, want ErrArchived", f, err)
|
||||
}
|
||||
}
|
||||
if got := e.pages.Fetched(); len(got) != 0 {
|
||||
t.Errorf("fetched %q for an archived dinner", got)
|
||||
}
|
||||
if after := e.dinner(t, id); after != before {
|
||||
t.Errorf("archived dinner changed: %+v, was %+v", after, before)
|
||||
}
|
||||
}
|
||||
|
||||
// waitFetched waits until rawURL has been fetched.
|
||||
func (e *env) waitFetched(t *testing.T, rawURL string) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(10 * time.Second)
|
||||
for !slices.Contains(e.pages.Fetched(), rawURL) {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("%s never fetched", rawURL)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user