Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
DevPlace CI / test (push) Failing after 44m45s
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:
@@ -78,6 +78,8 @@ class FakeTimers {
|
||||
}
|
||||
}
|
||||
|
||||
const NOW = Date.parse("2026-01-01T00:00:00Z");
|
||||
|
||||
function build(overrides = {}) {
|
||||
FakeSocket.instances = [];
|
||||
const timers = new FakeTimers();
|
||||
@@ -104,6 +106,7 @@ function build(overrides = {}) {
|
||||
WebSocketImpl: FakeSocket,
|
||||
timers,
|
||||
random: overrides.random || (() => 0.5),
|
||||
clock: () => NOW,
|
||||
clientVersion: "9.9.9",
|
||||
platform: "firefox",
|
||||
});
|
||||
@@ -321,3 +324,109 @@ test("settings_update can only lower the effective limits", async () => {
|
||||
assert.equal(context.limits.concurrency, 1);
|
||||
assert.equal(context.limits.bandwidthBytes, 10 * 1024 * 1024);
|
||||
});
|
||||
|
||||
function suspendedTicket(until) {
|
||||
return async () =>
|
||||
new Response(JSON.stringify({ error: { status: 403, message: "x", reason: "suspended", until } }), {
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
|
||||
test("a suspended ticket waits until the end instead of halting, then retries", async () => {
|
||||
const context = build({ random: () => 0, ticketFetch: suspendedTicket("2026-01-01T01:00:00Z") });
|
||||
await context.client.start();
|
||||
const snapshot = context.state.snapshot();
|
||||
assert.equal(snapshot.state, STATES.SUSPENDED);
|
||||
assert.equal(snapshot.suspendedUntil, "2026-01-01T01:00:00.000Z");
|
||||
assert.equal(context.timers.timeouts.at(-1).ms, 3600 * 1000);
|
||||
assert.equal(context.client.wanted, true);
|
||||
await context.client.ensure();
|
||||
assert.equal(FakeSocket.instances.length, 0);
|
||||
});
|
||||
|
||||
test("suspension waits are clamped and an unusable until waits five minutes", async () => {
|
||||
const cases = [
|
||||
["2025-12-31T00:00:00Z", 60],
|
||||
["2030-01-01T00:00:00Z", 24 * 3600],
|
||||
["", 300],
|
||||
["tomorrow", 300],
|
||||
];
|
||||
for (const [until, seconds] of cases) {
|
||||
const context = build({ random: () => 0, ticketFetch: suspendedTicket(until) });
|
||||
await context.client.start();
|
||||
assert.equal(context.timers.timeouts.at(-1).ms, seconds * 1000, until);
|
||||
assert.equal(context.state.snapshot().state, STATES.SUSPENDED);
|
||||
}
|
||||
const jittered = build({ random: () => 1, ticketFetch: suspendedTicket("") });
|
||||
await jittered.client.start();
|
||||
assert.equal(jittered.timers.timeouts.at(-1).ms, 330 * 1000);
|
||||
});
|
||||
|
||||
test("the retry after a suspension asks for a new ticket", async () => {
|
||||
let calls = 0;
|
||||
const context = build({
|
||||
random: () => 0,
|
||||
ticketFetch: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
return suspendedTicket("2026-01-01T00:02:00Z")();
|
||||
}
|
||||
return new Response(JSON.stringify({ ticket: "tk2", ws_path: "/nodes/ws" }), { status: 200 });
|
||||
},
|
||||
});
|
||||
await context.client.start();
|
||||
context.timers.timeouts.at(-1).fn();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(FakeSocket.instances.length, 1);
|
||||
});
|
||||
|
||||
test("a revoked ticket or other 403 reason still halts", async () => {
|
||||
const context = build({
|
||||
ticketFetch: async () =>
|
||||
new Response(JSON.stringify({ error: { status: 403, message: "x", reason: "revoked" } }), { status: 403 }),
|
||||
});
|
||||
await context.client.start();
|
||||
assert.equal(context.state.snapshot().state, STATES.HALTED);
|
||||
});
|
||||
|
||||
test("a suspended kick with until closes the socket and holds until then", async () => {
|
||||
const context = build({ random: () => 0 });
|
||||
const socket = await online(context);
|
||||
await socket.receive({ type: "kick", reason: "suspended", until: "2026-01-01T00:10:00Z" });
|
||||
assert.equal(context.state.snapshot().state, STATES.SUSPENDED);
|
||||
assert.equal(context.timers.timeouts.at(-1).ms, 600 * 1000);
|
||||
assert.equal(socket.closed.code, 1000);
|
||||
assert.equal(context.timers.intervals[0].cleared, true);
|
||||
});
|
||||
|
||||
test("a suspended kick without until waits five minutes", async () => {
|
||||
const context = build({ random: () => 0 });
|
||||
const socket = await online(context);
|
||||
await socket.receive({ type: "kick", reason: "suspended" });
|
||||
assert.equal(context.timers.timeouts.at(-1).ms, 300 * 1000);
|
||||
});
|
||||
|
||||
test("a close 4403 with reason suspended holds, any other 4403 halts", async () => {
|
||||
const context = build({ random: () => 0 });
|
||||
const socket = await online(context);
|
||||
await socket.serverClose(4403, "suspended");
|
||||
assert.equal(context.state.snapshot().state, STATES.SUSPENDED);
|
||||
});
|
||||
|
||||
test("welcome stores the server device label and clears an old suspension", async () => {
|
||||
const context = build();
|
||||
await context.state.set({ suspendedUntil: "2026-01-01T00:00:00.000Z" });
|
||||
await context.client.start();
|
||||
const socket = FakeSocket.instances[0];
|
||||
socket.open();
|
||||
await socket.receive(welcome({ device_label: "Work laptop" }));
|
||||
assert.equal(context.state.snapshot().deviceLabel, "Work laptop");
|
||||
assert.equal(context.state.snapshot().suspendedUntil, "");
|
||||
});
|
||||
|
||||
test("a welcome without a label leaves the label empty so the popup falls back", async () => {
|
||||
const context = build();
|
||||
await online(context);
|
||||
assert.equal(context.state.snapshot().deviceLabel, "");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user