Address Gadfly review on #14: bounds trap, date order, count cap
Build image / build-and-push (push) Successful in 5s
Build image / build-and-push (push) Successful in 5s
- UpdatePlanting: only re-check the object-bounds when the position is actually being moved. A plop orphaned outside its object by a later resize stays editable/removable instead of becoming a row you can't fix or delete. - finalizePlanting: reject removed_at before planted_at. - derivedCount: guard Inf (not just NaN) and cap the result at maxExplicitCount (1e6) — the same ceiling a manual override honors — so a huge radius / tiny spacing can't overflow a 32-bit int or return an absurd value. - Refresh stale docs that referenced #14 as not-yet-landed (FullGarden, ListActivePlantingsForGarden, ListReferencedPlants) and note the date-only layout beside timeLayout. Deliberately did NOT add a plantable re-check to Update/Delete: existing plops must stay editable/removable even if their object was later marked non-plantable (same trap as the bounds case). Tests: derived-count cap, removed-before-planted rejection, and edit/move of a plop orphaned by an object shrink. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -51,6 +51,14 @@ func TestDerivedCountFormula(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDerivedCountCapped(t *testing.T) {
|
||||
// A huge radius with a tiny spacing would produce an absurd (32-bit
|
||||
// overflowing) count; it's capped at the same ceiling as a manual override.
|
||||
if got := derivedCount(10_000, 0.1); got != maxExplicitCount {
|
||||
t.Errorf("derivedCount(10000, 0.1) = %d, want cap %d", got, maxExplicitCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePlantingDefaultsAndDerivedCount(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
@@ -230,8 +238,8 @@ func TestPlantingSoftRemoveLeavesFull(t *testing.T) {
|
||||
t.Errorf("full.Plants = %d, want 1 referenced plant", len(full.Plants))
|
||||
}
|
||||
|
||||
// Soft-remove → it leaves /full.
|
||||
rm := "2026-07-01"
|
||||
// Soft-remove → it leaves /full. (On/after today's default planted_at.)
|
||||
rm := "2030-06-01"
|
||||
if _, err := s.UpdatePlanting(context.Background(), owner, pl.ID,
|
||||
PlantingPatch{SetRemovedAt: true, RemovedAt: &rm}, pl.Version); err != nil {
|
||||
t.Fatalf("soft-remove: %v", err)
|
||||
@@ -242,6 +250,62 @@ func TestPlantingSoftRemoveLeavesFull(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlantingRemovedBeforePlantedRejected(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
bed := seedBed(t, s, owner, g.ID)
|
||||
plant := seedOwnPlant(t, s, owner, 10)
|
||||
|
||||
// planted_at defaults to today; a removed_at in the distant past is invalid.
|
||||
pl, _ := s.CreatePlanting(context.Background(), owner, bed.ID, PlantingInput{
|
||||
PlantID: plant.ID, XCM: 0, YCM: 0, RadiusCM: 10,
|
||||
})
|
||||
past := "2000-01-01"
|
||||
if _, err := s.UpdatePlanting(context.Background(), owner, pl.ID,
|
||||
PlantingPatch{SetRemovedAt: true, RemovedAt: &past}, pl.Version); !errors.Is(err, domain.ErrInvalidInput) {
|
||||
t.Errorf("removed-before-planted err = %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePlantingEditableAfterObjectShrinks(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
g := seedGarden(t, s, owner)
|
||||
bed := seedBed(t, s, owner, g.ID) // 200×200 → local bounds ±100
|
||||
plant := seedOwnPlant(t, s, owner, 10)
|
||||
|
||||
// A plop near the bed's edge.
|
||||
pl, _ := s.CreatePlanting(context.Background(), owner, bed.ID, PlantingInput{
|
||||
PlantID: plant.ID, XCM: 90, YCM: 0, RadiusCM: 10,
|
||||
})
|
||||
|
||||
// Shrink the bed to 100×100 (local bounds ±50): the plop's center (90) is now
|
||||
// outside the object.
|
||||
w, h := 100.0, 100.0
|
||||
if _, err := s.UpdateObject(context.Background(), owner, bed.ID, ObjectPatch{WidthCM: &w, HeightCM: &h}, bed.Version); err != nil {
|
||||
t.Fatalf("shrink bed: %v", err)
|
||||
}
|
||||
|
||||
// Editing the out-of-bounds plop without moving it (radius here) must still
|
||||
// work — otherwise it'd be un-removable.
|
||||
nr := 15.0
|
||||
updated, err := s.UpdatePlanting(context.Background(), owner, pl.ID, PlantingPatch{RadiusCM: &nr}, pl.Version)
|
||||
if err != nil {
|
||||
t.Fatalf("radius edit on orphaned plop should work: %v", err)
|
||||
}
|
||||
// But actually moving it still enforces the new bounds.
|
||||
outside := 90.0
|
||||
if _, err := s.UpdatePlanting(context.Background(), owner, pl.ID, PlantingPatch{XCM: &outside}, updated.Version); !errors.Is(err, domain.ErrInvalidInput) {
|
||||
t.Errorf("moving to out-of-bounds should be rejected: %v", err)
|
||||
}
|
||||
// Moving it back inside works.
|
||||
inside := 10.0
|
||||
if _, err := s.UpdatePlanting(context.Background(), owner, pl.ID, PlantingPatch{XCM: &inside}, updated.Version); err != nil {
|
||||
t.Errorf("moving in-bounds should work: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlantingVersionConflict(t *testing.T) {
|
||||
s := newTestService(t, openConfig())
|
||||
owner := seedUser(t, s, "[email protected]")
|
||||
|
||||
Reference in New Issue
Block a user