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
+81
View File
@@ -0,0 +1,81 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
from devplacepy.database import get_table
from devplacepy.utils import generate_uid
from tests.conftest import BASE_URL
def make_community(slug):
uid = generate_uid()
get_table("communities").insert(
{
"uid": uid, "slug": slug, "name": slug, "query": slug, "url": f"https://{slug}.example.org", "origin": f"{slug}.example.org",
"status": "ready", "enabled": 1, "interval_seconds": 86400, "run_count": 0, "failure_count": 0,
"created_at": datetime.now(timezone.utc).isoformat(), "deleted_at": None, "deleted_by": None, "schema": "{}",
}
)
return uid
def make_run(community_uid, **extra):
uid = generate_uid()
get_table("community_runs").insert(
{
"uid": uid, "community_uid": community_uid, "started_at": datetime.now(timezone.utc).isoformat(), "finished_at": "",
"status": "running", "dry_run": 0, "trigger": "admin", "collected": "", "pushed": "", "errors": "", "log": "first log line",
**extra,
}
)
return uid
def test_the_run_page_updates_itself_and_stops_polling_when_the_run_ends(alice):
page, _ = alice
slug = f"live{generate_uid()[-8:]}"
community = make_community(slug)
run = make_run(community)
page.goto(f"{BASE_URL}/admin/communities/{slug}/ledger?run={run}", wait_until="domcontentloaded")
root = page.locator("[data-community-root]")
root.wait_for(state="attached")
assert root.get_attribute("data-busy") == "true"
get_table("community_runs").update(
{
"uid": run, "status": "ok", "finished_at": datetime.now(timezone.utc).isoformat(), "verdict": "ok",
"headline": "Everything synced and updated by itself.", "tally": '{"created": 2}', "log": "final log line",
},
["uid"],
)
page.locator("text=Everything synced and updated by itself.").wait_for(state="visible", timeout=15000)
page.wait_for_function("document.querySelector('[data-community-root]').dataset.busy === 'false'", timeout=15000)
assert "final log line" in page.locator("pre.community-log").text_content()
def test_a_finished_run_page_does_not_poll(alice):
page, _ = alice
slug = f"idle{generate_uid()[-8:]}"
community = make_community(slug)
run = make_run(community, status="ok", finished_at=datetime.now(timezone.utc).isoformat(), verdict="ok", headline="Done.")
requests = []
page.on("request", lambda request: requests.append(request.url) if "/ledger" in request.url else None)
page.goto(f"{BASE_URL}/admin/communities/{slug}/ledger?run={run}", wait_until="domcontentloaded")
assert page.locator("[data-community-root]").get_attribute("data-busy") == "false"
page.wait_for_timeout(4500)
assert len(requests) == 1
def test_the_community_page_follows_a_running_run(alice):
page, _ = alice
slug = f"page{generate_uid()[-8:]}"
community = make_community(slug)
run = make_run(community)
page.goto(f"{BASE_URL}/admin/communities/{slug}", wait_until="domcontentloaded")
banner = page.locator("text=A run or a discovery is in progress")
banner.wait_for(state="visible")
get_table("community_runs").update(
{"uid": run, "status": "ok", "finished_at": datetime.now(timezone.utc).isoformat(), "verdict": "ok", "headline": "Finished while you watched."},
["uid"],
)
page.locator("text=Finished while you watched.").wait_for(state="visible", timeout=15000)
assert banner.count() == 0
+28
View File
@@ -496,3 +496,31 @@ def test_mobile_no_horizontal_overflow(mobile_page):
client_w = page.evaluate("document.documentElement.clientWidth")
assert scroll_w - client_w <= 1, f"horizontal overflow: {scroll_w} vs {client_w}"
assert page.locator("[data-game-grid]").first.is_visible()
def test_plot_stop_toggle_hidden_without_continuous_delivery(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
page.select_option("[data-slot='0'] select[name='crop']", "shell")
page.click("[data-slot='0'] button:has-text('Plant')")
page.locator("[data-slot='0'].game-plot-growing").wait_for(state="visible")
assert page.locator("form[data-game-action='plot-stop']").count() == 0
def test_plot_stop_toggle_flips_and_leaves_plot_empty_after_harvest(alice):
page, _ = alice
reset_farm("alice_test")
set_farm("alice_test", mastery_autoreplant=1)
open_game(page)
page.select_option("[data-slot='0'] select[name='crop']", "shell")
page.click("[data-slot='0'] button:has-text('Plant')")
toggle = page.locator("[data-slot='0'] form[data-game-action='plot-stop'] button")
toggle.wait_for(state="visible")
expect(toggle).to_contain_text("Stop after harvest")
toggle.click()
expect(toggle).to_contain_text("Resume auto-replant")
warp_ready("alice_test")
open_game(page)
page.locator("[data-slot='0'] form[data-game-action='harvest'] button").click()
page.locator("[data-slot='0'].game-plot-empty").wait_for(state="visible")
View File
+168
View File
@@ -0,0 +1,168 @@
# retoor <retoor@molodetz.nl>
import json
import os
import shutil
import socket
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
import pytest
from devplacepy.database import get_table, init_db, set_consent
from devplacepy.database.nodes import create_device, save_node_settings, suspend_device, unsuspend_device
from devplacepy.services.nodes.broker import JobBroker
from devplacepy.services.nodes.policy import DestinationPolicy
from devplacepy.utils import register_account
from tests.conftest import PROJECT_ROOT
EXTENSION_SOURCE = Path(PROJECT_ROOT) / "nodes" / "extension"
JOB_URL = "https://www.reddit.com/r/python.json"
def free_port() -> int:
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
return probe.getsockname()[1]
@pytest.fixture(scope="module")
def donor():
init_db()
name = f"extdonor{int(time.time() * 1000)}"
uid, _, _ = register_account(name, f"{name}@t.dev", "secret123", age_band="adult", accepted_terms=True)
set_consent("user", uid, "bandwidth_sharing", True)
save_node_settings(uid, {"enabled": True})
device, secret = create_device(uid, "Browser under test", "extension")
return {"uid": uid, "device": device, "secret": secret}
@pytest.fixture(scope="module")
def node_server(donor):
port = free_port()
log = open(f"/tmp/node_app_{port}.log", "w")
proc = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "--factory", "tests.e2e.nodes.node_app:build", "--host", "127.0.0.1", "--port", str(port), "--log-level", "warning"],
cwd=str(PROJECT_ROOT),
env={**os.environ, "PYTHONUNBUFFERED": "1"},
stdout=log,
stderr=subprocess.STDOUT,
)
base = f"http://127.0.0.1:{port}"
deadline = time.time() + 30
while time.time() < deadline:
try:
urllib.request.urlopen(f"{base}/stats", timeout=2)
break
except Exception:
if proc.poll() is not None:
raise RuntimeError(f"node app died, see /tmp/node_app_{port}.log")
time.sleep(0.3)
else:
proc.terminate()
raise RuntimeError("node app did not start")
yield base
proc.terminate()
proc.wait(timeout=10)
from devplacepy.database import refresh_snapshot, set_setting
refresh_snapshot()
set_setting("nodes_enabled", "0")
set_setting("nodes_allow_unknown_class", "0")
def stats(base: str) -> dict:
return json.loads(urllib.request.urlopen(f"{base}/stats", timeout=5).read())
def wait_for(predicate, seconds: float = 20.0, message: str = "condition"):
deadline = time.time() + seconds
while time.time() < deadline:
value = predicate()
if value:
return value
time.sleep(0.2)
raise AssertionError(f"{message} never became true")
@pytest.fixture
def extension_dir(tmp_path):
target = tmp_path / "extension"
shutil.copytree(EXTENSION_SOURCE, target, ignore=shutil.ignore_patterns("test", "node_modules", "*.zip"))
manifest = json.loads((target / "manifest.json").read_text())
manifest["host_permissions"].append("http://127.0.0.1/*")
(target / "manifest.json").write_text(json.dumps(manifest))
return target
@pytest.fixture
def browser_with_extension(playwright_instance, extension_dir, tmp_path):
context = playwright_instance.chromium.launch_persistent_context(
str(tmp_path / "profile"),
headless=False,
args=[
"--headless=new",
f"--disable-extensions-except={extension_dir}",
f"--load-extension={extension_dir}",
"--no-sandbox",
],
)
worker = context.service_workers[0] if context.service_workers else context.wait_for_event("serviceworker", timeout=20000)
extension_id = worker.url.split("/")[2]
yield context, extension_id
context.close()
def command(page, message: dict) -> dict:
return page.evaluate("(message) => chrome.runtime.sendMessage(message)", message)
def popup(context, extension_id):
page = context.new_page()
page.goto(f"chrome-extension://{extension_id}/ui/popup.html", wait_until="domcontentloaded")
return page
def test_the_extension_connects_serves_a_job_gets_suspended_and_disconnects(node_server, donor, browser_with_extension):
context, extension_id = browser_with_extension
page = popup(context, extension_id)
assert command(page, {"cmd": "status"})["state"]["state"] == "off"
page.locator("#disclosure-card").wait_for(state="visible")
disclosure = context.new_page()
disclosure.goto(f"chrome-extension://{extension_id}/ui/disclosure.html", wait_until="domcontentloaded")
disclosure.locator("#accept").click()
disclosure.locator("#done").filter(has_text="Accepted").wait_for(state="visible")
reply = command(page, {"cmd": "connect", "serverUrl": node_server, "secret": donor["secret"], "label": "typed locally"})
assert reply["config"]["hasSecret"] is True and reply["config"]["secretPrefix"].startswith("dpn_")
assert "dpn_" not in json.dumps(reply)[len('{"config":'):].replace(reply["config"]["secretPrefix"], "")
assert command(page, {"cmd": "enable", "enabled": True})["config"]["enabled"] is True
wait_for(lambda: stats(node_server)["connected"] == 1, message="the extension connecting")
page.reload(wait_until="domcontentloaded")
page.locator("#state").filter(has_text="Online").wait_for(state="visible", timeout=15000)
assert page.locator("#label").inner_text() == "Browser under test"
target = DestinationPolicy().check(JOB_URL, "GET", "fetch")
uid = JobBroker().enqueue("guest", "browser-e2e", target, "fetch")
def finished():
row = get_table("node_requests").find_one(uid=uid)
return row if row and row["status"] in ("done", "failed") else None
row = wait_for(finished, seconds=40, message="the job being served")
assert row["mode"] == "fetch"
assert row["device_uid"] == donor["device"]["uid"] and row["device_asn_class"] == "unknown"
assert row["error"] not in ("node_disconnected", "timeout", "send_failed")
view = command(page, {"cmd": "status"})
assert view["usage"]["requests"] + view["usage"]["failed"] >= 1
suspend_device(donor["device"]["uid"], "browser e2e", 30)
wait_for(lambda: stats(node_server)["connected"] == 0, message="the suspension disconnecting the extension")
suspended = wait_for(lambda: (state := command(page, {"cmd": "status"})["state"])["state"] == "suspended" and state, message="the suspended state")
assert suspended["suspendedUntil"].startswith("20")
page.reload(wait_until="domcontentloaded")
page.locator("#detail").filter(has_text="Suspended until").wait_for(state="visible", timeout=10000)
unsuspend_device(donor["device"]["uid"])
assert command(page, {"cmd": "enable", "enabled": False})["config"]["enabled"] is False
wait_for(lambda: stats(node_server)["connected"] == 0, message="the switch disconnecting")
assert command(page, {"cmd": "status"})["state"]["state"] == "off"
+38
View File
@@ -0,0 +1,38 @@
# retoor <retoor@molodetz.nl>
import asyncio
from fastapi import FastAPI
def build() -> FastAPI:
from devplacepy.database import init_db, refresh_snapshot, set_setting
from devplacepy.routers.nodes import router
from devplacepy.services.manager import service_manager
from devplacepy.services.nodes.service import NodesService
app = FastAPI()
app.include_router(router, prefix="/nodes")
service = NodesService()
@app.on_event("startup")
async def start() -> None:
init_db()
set_setting("nodes_enabled", "1")
set_setting("nodes_allow_unknown_class", "1")
service_manager.register(service)
service_manager.set_lock_owner(True)
service_manager.supervise()
service.scheduler.destination_ready = lambda target: True
await asyncio.sleep(0)
@app.middleware("http")
async def fresh_snapshot(request, call_next):
refresh_snapshot()
return await call_next(request)
@app.get("/stats")
async def stats() -> dict:
return {"connected": service.registry.count(), "counters": service.counters.snapshot()}
return app