Agent: catalog and garden tools, and a ready date on every describe group
Build image / build-and-push (push) Successful in 11s
Gadfly review (reusable) / review (pull_request) Successful in 4m42s
Adversarial Review (Gadfly) / review (pull_request) Successful in 4m42s

- update_seed_lot / delete_seed_lot: correct or drop a recorded purchase
  ("it was three packets, not two"); the plant a lot is for stays fixed.
- delete_plant: remove a duplicate from the user's catalog. The service
  already refuses while plantings (past seasons included) or a lot reference
  it; the tool turns that sentinel into words the model can pass on, and
  tells it not to clear those references to get its way.
- create_garden: a new place, with the service's defaults; the prompt says a
  plan is still a copy_garden.
- describe_garden groups carry readyAround — planting date plus days to
  maturity for the plops still in the ground — so "what can I pick this
  week?" is a lookup rather than arithmetic the model got wrong live.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-23 02:05:19 -04:00
co-authored by Claude Fable 5
parent 35b27de8a0
commit b4c8007977
7 changed files with 299 additions and 3 deletions
+64
View File
@@ -920,3 +920,67 @@ func TestDescribeGardenByYear(t *testing.T) {
t.Errorf("describe(20026) err = %v, want ErrInvalidInput", err)
}
}
// TestDescribeGroupSaysWhenReady — "what can I pick this week?" is a lookup
// when the group carries the date, and a sum the model gets wrong when it
// doesn't. Planting date plus days to maturity, for the plops still in the
// ground; nothing for a plant the catalog has no days for.
func TestDescribeGroupSaysWhenReady(t *testing.T) {
ctx := context.Background()
s := newTestService(t, openConfig())
owner := seedUser(t, s, "[email protected]")
g, err := s.CreateGarden(ctx, owner, GardenInput{Name: "Harvest", WidthCM: 2000, HeightCM: 2000})
if err != nil {
t.Fatalf("garden: %v", err)
}
bed := seedFillBed(t, s, owner, g.ID, 400, 400)
sixty := 60
radish, err := s.CreatePlant(ctx, owner, PlantInput{Name: "Radish", Category: domain.CategoryVegetable, SpacingCM: 5, Color: "#c33", Icon: "🌱", DaysToMaturity: &sixty})
if err != nil {
t.Fatalf("radish: %v", err)
}
mint := seedNamedPlant(t, s, owner, "Mint", 30) // no days to maturity
plant := func(plantID int64, x float64, on string) *domain.Planting {
t.Helper()
pl, err := s.CreatePlanting(ctx, owner, bed.ID, PlantingInput{PlantID: plantID, XCM: x, YCM: 0, RadiusCM: 10, PlantedAt: &on})
if err != nil {
t.Fatalf("plant: %v", err)
}
return pl
}
plant(radish.ID, -100, "2026-05-01")
plant(radish.ID, 0, "2026-05-11")
pulled := plant(radish.ID, 100, "2026-03-01")
on := "2026-04-20"
if _, err := s.RemovePlanting(ctx, owner, pulled.ID, pulled.Version, &on); err != nil {
t.Fatalf("pull: %v", err)
}
plant(mint.ID, 150, "2026-05-01")
groups := func(year *int) map[string]DescribeGroup {
t.Helper()
desc, err := s.DescribeGarden(ctx, owner, g.ID, year)
if err != nil {
t.Fatalf("describe: %v", err)
}
out := map[string]DescribeGroup{}
for _, gr := range desc.Objects[0].Plantings {
out[gr.Plant] = gr
}
return out
}
now := groups(nil)
if got := now["Radish"].ReadyAround; got != "2026-06-30…2026-07-10" {
t.Errorf("radish readyAround = %q, want %q", got, "2026-06-30…2026-07-10")
}
if got := now["Mint"].ReadyAround; got != "" {
t.Errorf("mint has no days to maturity but readyAround = %q", got)
}
// The season view lists the pulled radish too, but its harvest is behind
// us: the range is still the two still growing.
y := 2026
if got := groups(&y)["Radish"]; got.Removed != 1 || got.ReadyAround != "2026-06-30…2026-07-10" {
t.Errorf("2026 radish = removed %d, readyAround %q; want 1 and the live plops' range", got.Removed, got.ReadyAround)
}
}