Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
DevPlace CI / test (push) Failing after 44m45s

Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 02:23:43 +02:00
co-authored by Claude Sonnet 5.5
parent ce899fc5e1
commit 02ab747cd0
203 changed files with 11624 additions and 301 deletions
+91
View File
@@ -1,6 +1,7 @@
package main
import (
"errors"
"path/filepath"
"testing"
"time"
@@ -160,3 +161,93 @@ func TestUsagePersistsAcrossRestart(t *testing.T) {
t.Fatalf("read usage %d", got)
}
}
func TestAcquireRefusesOnBatteryOnlyWhenAsked(t *testing.T) {
limiter, c := testLimiter(t, DefaultConfig(), "2026-01-15T12:00:00Z")
calls := 0
onBattery := true
limiter.SetBatteryProbe(func() (bool, error) { calls++; return onBattery, nil }, nil)
if got := limiter.Acquire(); got != "" {
t.Fatalf("setting off must ignore the probe: %q", got)
}
limiter.Release()
if calls != 0 {
t.Fatalf("probe ran %d times with the setting off", calls)
}
limiter.ApplyServer(nil, &ServerSettings{PauseOnBattery: true})
if got := limiter.Acquire(); got != RefuseBattery {
t.Fatalf("got %q", got)
}
onBattery = false
if got := limiter.Acquire(); got != RefuseBattery || calls != 1 {
t.Fatalf("cached result expected: %q calls %d", got, calls)
}
c.now = c.now.Add(16 * time.Second)
if got := limiter.Acquire(); got != "" || calls != 2 {
t.Fatalf("after cache expiry: %q calls %d", got, calls)
}
}
func TestBatteryProbeErrorMeansMainsAndLogsOnce(t *testing.T) {
limiter, c := testLimiter(t, DefaultConfig(), "2026-01-15T12:00:00Z")
logged := 0
limiter.SetBatteryProbe(func() (bool, error) { return true, errors.New("no sysfs") }, func(string, ...any) { logged++ })
limiter.ApplyServer(nil, &ServerSettings{PauseOnBattery: true})
for i := 0; i < 3; i++ {
if got := limiter.Acquire(); got != "" {
t.Fatalf("probe error must not refuse: %q", got)
}
limiter.Release()
c.now = c.now.Add(20 * time.Second)
}
if logged != 1 {
t.Fatalf("logged %d times", logged)
}
}
func TestBatteryHelpers(t *testing.T) {
if !parsePmset("Now drawing from 'Battery Power'\n -InternalBattery-0") || parsePmset("Now drawing from 'AC Power'") {
t.Fatal("pmset parse")
}
cases := []struct {
ac, flag byte
want bool
}{{0, 1, true}, {0, 8, true}, {1, 1, false}, {0, 128, false}, {255, 255, false}, {0, 255, false}}
for _, item := range cases {
if got := windowsOnBattery(item.ac, item.flag); got != item.want {
t.Errorf("ac %d flag %d: %v", item.ac, item.flag, got)
}
}
}
const (
parityBase = 1767225600
parityStep = 40009
paritySamples = 200
)
var parityCases = []struct{ from, to, zone, bits string }{
{"09:00", "17:00", "Europe/Amsterdam", "01010000001010101010000101010101000000101010100000010101010000001010101010000101010101000000101010100000010101010000001010101010000101010101000000101010100000010101010000001010101010000001010101010000"},
{"22:00", "06:00", "America/New_York", "00010101010000001010101000000101010101000010101010100000010101010000001010101000000101010101000010101010100000010101010000001010101000000101010101000000101010101000010101010100000010101010000001010101"},
{"08:30", "12:15", "Asia/Kolkata", "00000001010000000000101000000000010101000000000010100000000001010000000000101000000000000101000000000010100000000001010000000000101000000000000101000000000010100000000001010000000000101010000000000101"},
{"00:00", "12:00", "Pacific/Auckland", "01101010101010110101010101010010101010101001010101010101101010101010110101010101010010101010101001010101010101101010101010110101010101010010101010101011010101010101101010101010100101010101010010101010"},
{"10:00", "10:00", "UTC", "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"},
{"09:00", "17:00", "Not/AZone", "01010000101010101000000101010100000010101010000001010101010000001010101000000101010100000010101010100001010101010000001010101000000101010100000010101010100001010101010000001010101000000101010100000010"},
{"23:00", "01:00", "Australia/Lord_Howe", "00000000000010000000000001010000000000001000000000000100000000000010000000000001010000000000001000000000000100000000000010000000000000010000000000001000000000000100000000000010000000000000010000000000"},
{"", "", "Europe/Amsterdam", "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"},
}
func TestInWindowParityWithPythonScheduler(t *testing.T) {
for _, item := range parityCases {
zone, err := time.LoadLocation(item.zone)
if err != nil {
zone = time.UTC
}
for index := 0; index < paritySamples; index++ {
moment := time.Unix(int64(parityBase+index*parityStep), 0).In(zone)
if got := InWindow(item.from, item.to, moment); got != (item.bits[index] == '1') {
t.Fatalf("%s-%s %s #%d: got %v", item.from, item.to, item.zone, index, got)
}
}
}
}