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:
@@ -0,0 +1,109 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.database import get_table
|
||||
from tests.api.sync.conftest import comment_record, post_record, sync_ok, unique, user_record
|
||||
|
||||
|
||||
def listing(client, admin, origin, **params):
|
||||
return client.get("/sync/deletions", params={"origin": origin, **params}, headers=admin["headers"])
|
||||
|
||||
|
||||
def ack(client, admin, origin, items):
|
||||
return client.post("/sync/deletions/ack", json={"origin": origin, "items": items}, headers=admin["headers"])
|
||||
|
||||
|
||||
def seed(client, admin, origin):
|
||||
sync_ok(client, admin, "users", origin, [user_record("u1", unique("del"))])
|
||||
sync_ok(client, admin, "posts", origin, [post_record("p1", "u1"), post_record("p2", "u1"), post_record("p3", "u1")])
|
||||
sync_ok(client, admin, "comments", origin, [comment_record("c1", "p1", "u1")])
|
||||
|
||||
|
||||
def local(origin, kind, origin_uid):
|
||||
return get_table("sync_links").find_one(origin=origin, kind=kind, origin_uid=origin_uid)["local_uid"]
|
||||
|
||||
|
||||
def test_authz(client, admin):
|
||||
from tests.api.sync.conftest import make_account
|
||||
|
||||
assert client.get("/sync/deletions", params={"origin": "a.b"}).status_code == 401
|
||||
assert listing(client, make_account("Member"), "a.b").status_code == 403
|
||||
assert client.get("/sync/deletions", headers=admin["headers"]).status_code == 422
|
||||
|
||||
|
||||
def test_nothing_is_listed_for_a_clean_origin_and_sync_own_deletions_never_are(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
assert listing(client, admin, origin).json()["items"] == []
|
||||
sync_ok(client, admin, "posts", origin, [post_record("p1", "u1", updated_at="2025-05-01T00:00:00Z", deleted=True)])
|
||||
assert listing(client, admin, origin).json()["items"] == []
|
||||
|
||||
|
||||
def test_a_member_deletion_a_system_deletion_and_a_purge_are_told_apart(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "someone"}, ["uid"])
|
||||
get_table("posts").update({"uid": local(origin, "posts", "p2"), "deleted_at": "2026-01-02T00:00:00+00:00", "deleted_by": "system"}, ["uid"])
|
||||
purged_uid = local(origin, "posts", "p3")
|
||||
get_table("posts").delete(uid=purged_uid)
|
||||
body = listing(client, admin, origin).json()
|
||||
found = {(item["kind"], item["origin_uid"]): item["by"] for item in body["items"]}
|
||||
assert found == {("posts", "p1"): "member", ("posts", "p2"): "system", ("posts", "p3"): "purged"}
|
||||
assert body["count"] == 3 and body["has_more"] is False
|
||||
|
||||
|
||||
def test_paging_with_a_cursor_covers_every_entry_once(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
for origin_uid in ("p1", "p2", "p3"):
|
||||
get_table("posts").update({"uid": local(origin, "posts", origin_uid), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"])
|
||||
get_table("comments").update({"uid": local(origin, "comments", "c1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"])
|
||||
seen, cursor = [], ""
|
||||
while True:
|
||||
body = listing(client, admin, origin, limit=2, cursor=cursor).json()
|
||||
seen += [(item["kind"], item["origin_uid"]) for item in body["items"]]
|
||||
cursor = body["next_cursor"]
|
||||
if not body["has_more"]:
|
||||
break
|
||||
assert sorted(seen) == [("comments", "c1"), ("posts", "p1"), ("posts", "p2"), ("posts", "p3")]
|
||||
assert listing(client, admin, origin, cursor="zz").status_code == 400
|
||||
|
||||
|
||||
def test_other_origins_are_never_listed(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"])
|
||||
assert listing(client, admin, f"{unique('x')}.example.org").json()["items"] == []
|
||||
|
||||
|
||||
def test_acknowledging_stops_the_listing_and_quiets_the_resend(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
purged = local(origin, "posts", "p3")
|
||||
get_table("posts").delete(uid=purged)
|
||||
resend = sync_ok(client, admin, "posts", origin, [post_record("p3", "u1", updated_at="2025-09-01T00:00:00Z")])["results"][0]
|
||||
assert resend["status"] == "rejected" and resend["reason"] == "purged_locally"
|
||||
response = ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p3"}])
|
||||
assert response.status_code == 200 and response.json()["acknowledged"] == 1
|
||||
assert listing(client, admin, origin).json()["items"] == []
|
||||
again = sync_ok(client, admin, "posts", origin, [post_record("p3", "u1", updated_at="2025-10-01T00:00:00Z")])["results"][0]
|
||||
assert again["status"] == "unchanged" and again["reason"] == "acknowledged_deleted"
|
||||
assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p3"}]).json()["acknowledged"] == 0
|
||||
|
||||
|
||||
def test_a_locally_deleted_row_after_ack_answers_acknowledged(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
get_table("posts").update({"uid": local(origin, "posts", "p1"), "deleted_at": "2026-01-01T00:00:00+00:00", "deleted_by": "m"}, ["uid"])
|
||||
ack(client, admin, origin, [{"kind": "posts", "origin_uid": "p1"}])
|
||||
result = sync_ok(client, admin, "posts", origin, [post_record("p1", "u1", content="edited after local delete", updated_at="2025-09-01T00:00:00Z")])["results"][0]
|
||||
assert result["status"] == "unchanged" and result["reason"] == "acknowledged_deleted"
|
||||
|
||||
|
||||
def test_ack_validation_and_audit(client, admin, origin):
|
||||
assert ack(client, admin, origin, []).status_code == 422
|
||||
assert ack(client, admin, origin, [{"kind": "gists", "origin_uid": "x"}]).status_code == 422
|
||||
assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "bad id"}]).status_code == 422
|
||||
assert ack(client, admin, origin, [{"kind": "posts", "origin_uid": "unknown-one"}]).json()["acknowledged"] == 0
|
||||
rows = list(get_table("audit_log").find(event_key="sync.deletions.ack", actor_uid=admin["uid"]))
|
||||
assert rows
|
||||
|
||||
|
||||
def test_a_purged_user_is_listed(client, admin, origin):
|
||||
seed(client, admin, origin)
|
||||
get_table("users").delete(uid=local(origin, "users", "u1"))
|
||||
items = listing(client, admin, origin).json()["items"]
|
||||
assert ("users", "u1", "purged") in {(item["kind"], item["origin_uid"], item["by"]) for item in items}
|
||||
Reference in New Issue
Block a user