Address seed-packet review: 413 mapping, deadline, rollback, dedup
Build image / build-and-push (push) Successful in 6s
Build image / build-and-push (push) Successful in 6s
Gadfly findings on #94, the real ones: - scanSeedPacket extends only the READ deadline; a slow upload + a live vision call runs past the server's absolute 30s WriteTimeout and the successful response is silently dropped (the #78 failure mode). Extend the write deadline too (scanWriteTimeout). - An oversized upload tripping MaxBytesReader was mapped to 400; it's 413. Detect *http.MaxBytesError and report IMAGE_TOO_LARGE. - Split imagenorm error mapping: ErrTooLarge->413, ErrUnsupported->400, genuine read/encode faults (and a failed file.Open)->500, not 400. - CreateFromPacket discarded the plant it created when the lot then failed, contradicting its own doc. Roll the new plant back instead so the confirm is all-or-nothing (a fresh plant has no lots/plantings, so the delete is safe; log-and-continue on cleanup failure). - Dedup: packetLotRequest and seedLotCreateRequest shared every lot field. Extract a seedLotFields base both use. validCategory now reuses plantCategories. EffectiveConfig resolves agent+vision from one settings-row read instead of two. - capabilities swallowed an EffectiveVision error silently; log it. - vision test hand-copied Extract's body (drift risk). Split generate() out of Extract so the hermetic test drives the real request builder. Tests: rollback-on-lot-failure (service), oversized->413 (api). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -85,14 +86,13 @@ func suggestedName(p vision.SeedPacket) string {
|
||||
}
|
||||
|
||||
// validCategory returns the packet's category if it's one pansy knows, else "".
|
||||
// It reuses plantCategories — the same set CreatePlant validates against — so a
|
||||
// new category can't be accepted by one path and rejected by the other.
|
||||
func validCategory(c string) string {
|
||||
switch c {
|
||||
case domain.CategoryVegetable, domain.CategoryHerb, domain.CategoryFlower,
|
||||
domain.CategoryFruit, domain.CategoryTreeShrub, domain.CategoryCover:
|
||||
if _, ok := plantCategories[c]; ok {
|
||||
return c
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// matchPlants ranks existing plants the packet might already be, best first.
|
||||
@@ -179,10 +179,12 @@ type PacketResult struct {
|
||||
// history — plants and lots are catalog/inventory, created directly — so there is
|
||||
// no change set to wrap; the two creations just happen in sequence.
|
||||
//
|
||||
// If a new plant is created but the lot then fails, the plant is left behind
|
||||
// rather than rolled back: a stray catalog entry is harmless and editable,
|
||||
// whereas silently discarding a variety the user just confirmed is worse. The
|
||||
// caller sees the error and can retry the lot against the now-existing plant.
|
||||
// If a new plant is created but the lot then fails, the plant is rolled back so
|
||||
// the confirm is all-or-nothing. Otherwise a bad lot (say, an invalid unit) would
|
||||
// strand a half-made catalog entry the user never asked for on its own, and the
|
||||
// error return gives the HTTP caller no handle to it. A just-created plant has no
|
||||
// plantings or lots yet, so the delete is safe; if it somehow can't be undone we
|
||||
// log and still surface the original lot error, not the cleanup one.
|
||||
func (s *Service) CreateFromPacket(ctx context.Context, actorID int64, in PacketConfirm) (*PacketResult, error) {
|
||||
hasID, hasNew := in.PlantID != nil, in.NewPlant != nil
|
||||
if hasID == hasNew {
|
||||
@@ -211,6 +213,13 @@ func (s *Service) CreateFromPacket(ctx context.Context, actorID int64, in Packet
|
||||
lotIn.PlantID = res.Plant.ID
|
||||
lot, err := s.CreateSeedLot(ctx, actorID, lotIn)
|
||||
if err != nil {
|
||||
if res.PlantIsNew {
|
||||
// Roll back the plant we just made for this confirm; keep the lot error.
|
||||
if delErr := s.DeletePlant(ctx, actorID, res.Plant.ID); delErr != nil {
|
||||
slog.Error("service: could not roll back plant after packet lot failed",
|
||||
"error", delErr, "plant", res.Plant.ID)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
res.Lot = lot
|
||||
|
||||
Reference in New Issue
Block a user