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
+112 -5
View File
@@ -268,12 +268,12 @@ func TestSessionLimitsAnswerWithDonorCodes(t *testing.T) {
_ = c.WriteClose(CloseGoingAway, "bye")
return
}
writeFrame(t, c, map[string]any{"type": "kick", "reason": "suspended"})
writeFrame(t, c, map[string]any{"type": "kick", "reason": "revoked"})
})
h.session.limiter.AddBytes(1 << 40)
err := h.run(t)
var fatal *FatalError
if !errors.As(err, &fatal) || fatal.Reason != "suspended" {
if !errors.As(err, &fatal) || fatal.Reason != "revoked" {
t.Fatalf("run returned %v", err)
}
}
@@ -286,7 +286,8 @@ func TestFatalTicketStatuses(t *testing.T) {
}{
{401, "", "unauthorized"},
{403, `{"error":{"status":403,"message":"x","reason":"no_consent"}}`, "no_consent"},
{403, `{"error":{"status":403,"message":"x","reason":"suspended"}}`, "suspended"},
{403, `{"error":{"status":403,"message":"x","reason":"revoked"}}`, "revoked"},
{403, `{"error":{"status":403,"message":"x","reason":"kill_switch"}}`, "kill_switch"},
}
for _, item := range cases {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -349,14 +350,120 @@ func TestBackoffStaysInsideBounds(t *testing.T) {
}
func TestFatalReasonsMatchPlan(t *testing.T) {
for _, reason := range []string{"revoked", "suspended", "kill_switch", "no_consent"} {
for _, reason := range []string{"revoked", "kill_switch", "no_consent"} {
if !FatalReasons[reason] {
t.Errorf("%s must stop reconnecting", reason)
}
}
for _, reason := range []string{"heartbeat", "ticket", "capacity"} {
for _, reason := range []string{"heartbeat", "ticket", "capacity", "suspended"} {
if FatalReasons[reason] {
t.Errorf("%s must reconnect", reason)
}
}
}
type sleepLog struct{ waits []time.Duration }
func (l *sleepLog) sleep(_ context.Context, d time.Duration) { l.waits = append(l.waits, d) }
func fixedNow(text string) func() time.Time {
moment, _ := time.Parse(time.RFC3339, text)
return func() time.Time { return moment }
}
func TestSuspendedTicketWaitsUntilTheEndThenRetries(t *testing.T) {
cases := []struct {
name string
until string
want time.Duration
}{
{"future", "2026-01-01T01:00:00Z", time.Hour},
{"empty", "", 300 * time.Second},
{"garbage", "tomorrow", 300 * time.Second},
{"expired", "2025-12-31T00:00:00Z", 60 * time.Second},
{"far", "2030-01-01T00:00:00Z", 24 * time.Hour},
}
for _, item := range cases {
t.Run(item.name, func(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if calls.Add(1) == 1 {
w.WriteHeader(http.StatusForbidden)
body, _ := json.Marshal(map[string]any{"error": map[string]any{"status": 403, "message": "x", "reason": "suspended", "until": item.until}})
_, _ = w.Write(body)
return
}
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
cfg := DefaultConfig()
cfg.Server, cfg.Secret = server.URL, testSecret
limiter, _ := NewLimiter(cfg, "", time.Now)
policy := testPolicy(publicAnswer)
session := NewSession(cfg, policy, limiter, NewFetcher(policy), NewStats(time.Now()), NewLogger(testWriter{t}, true))
session.jitter = func() float64 { return 0 }
session.now = fixedNow("2026-01-01T00:00:00Z")
slept := &sleepLog{}
session.sleep = slept.sleep
var fatal *FatalError
if err := session.Run(context.Background()); !errors.As(err, &fatal) || calls.Load() != 2 {
t.Fatalf("err %v calls %d", err, calls.Load())
}
if len(slept.waits) != 1 || slept.waits[0] != item.want {
t.Fatalf("waits %v want %v", slept.waits, item.want)
}
})
}
}
func TestSuspendedKickHoldsThenReconnects(t *testing.T) {
h := newHarness(t, func(n int, c *Conn) {
readFrame(t, c)
writeFrame(t, c, welcome())
if n == 1 {
writeFrame(t, c, map[string]any{"type": "kick", "reason": "suspended", "until": "2026-01-01T00:10:00Z"})
return
}
writeFrame(t, c, map[string]any{"type": "kick", "reason": "revoked"})
})
h.session.now = fixedNow("2026-01-01T00:00:00Z")
slept := &sleepLog{}
h.session.sleep = slept.sleep
var fatal *FatalError
if err := h.run(t); !errors.As(err, &fatal) || fatal.Reason != "revoked" {
t.Fatalf("run returned %v", err)
}
if h.conns.Load() != 2 || len(slept.waits) != 1 || slept.waits[0] != 10*time.Minute {
t.Fatalf("conns %d waits %v", h.conns.Load(), slept.waits)
}
}
func TestSuspendedWaitHonoursCancel(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":{"status":403,"message":"x","reason":"suspended","until":""}}`))
}))
defer server.Close()
cfg := DefaultConfig()
cfg.Server, cfg.Secret = server.URL, testSecret
limiter, _ := NewLimiter(cfg, "", time.Now)
policy := testPolicy(publicAnswer)
session := NewSession(cfg, policy, limiter, NewFetcher(policy), NewStats(time.Now()), NewLogger(testWriter{t}, true))
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
started := time.Now()
if err := session.Run(ctx); err != nil || time.Since(started) > 5*time.Second {
t.Fatalf("err %v after %v", err, time.Since(started))
}
}
func TestWelcomeDecodesDeviceLabel(t *testing.T) {
frame, err := decodeFrame([]byte(`{"type":"welcome","device_uid":"d","device_label":"Laptop","settings":{"pause_on_battery":true}}`))
if err != nil || frame.DeviceLabel != "Laptop" || !frame.Settings.PauseOnBattery {
t.Fatalf("%+v %v", frame, err)
}
kick, _ := decodeFrame([]byte(`{"type":"kick","reason":"suspended","until":"2026-01-01T00:00:00Z"}`))
if kick.Until == "" {
t.Fatal("until not decoded")
}
}