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>
150 lines
5.6 KiB
Python
150 lines
5.6 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from devplacepy.services.sync.records import (
|
|
CommentRecord,
|
|
PostRecord,
|
|
SyncBatchForm,
|
|
UserRecord,
|
|
)
|
|
from devplacepy.services.sync.spec import LIMITS
|
|
|
|
BASE = {"origin_uid": "r1", "created_at": "2025-01-01T00:00:00Z"}
|
|
|
|
|
|
def test_updated_at_defaults_to_created_and_never_precedes_it():
|
|
record = PostRecord.model_validate(
|
|
{**BASE, "author_uid": "u", "content": "ten chars!!"}
|
|
)
|
|
assert record.updated_at == record.created_at
|
|
earlier = PostRecord.model_validate(
|
|
{**BASE, "author_uid": "u", "content": "ten chars!!", "updated_at": "2024-01-01"}
|
|
)
|
|
assert earlier.updated_at == earlier.created_at
|
|
|
|
|
|
def test_unknown_topic_becomes_random_and_known_is_kept():
|
|
base = {**BASE, "author_uid": "u", "content": "ten chars!!"}
|
|
assert PostRecord.model_validate({**base, "topic": "zzz"}).topic == "random"
|
|
assert PostRecord.model_validate({**base, "topic": "DevLog"}).topic == "devlog"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"payload",
|
|
[
|
|
{"author_uid": "u", "content": "short"},
|
|
{"author_uid": "u", "content": "x" * (LIMITS.max_post_length + 1)},
|
|
{"author_uid": "", "content": "ten chars!!"},
|
|
{"content": "ten chars!!"},
|
|
{"author_uid": "u", "content": "ten chars!!", "title": "t" * (LIMITS.max_title_length + 1)},
|
|
],
|
|
)
|
|
def test_invalid_posts_are_rejected(payload):
|
|
with pytest.raises(ValidationError):
|
|
PostRecord.model_validate({**BASE, **payload})
|
|
|
|
|
|
def test_comment_parent_is_optional_and_post_is_required():
|
|
base = {**BASE, "author_uid": "u", "content": "fine"}
|
|
assert CommentRecord.model_validate({**base, "post_uid": "p"}).parent_uid is None
|
|
assert CommentRecord.model_validate({**base, "post_uid": "p", "parent_uid": ""}).parent_uid is None
|
|
with pytest.raises(ValidationError):
|
|
CommentRecord.model_validate(base)
|
|
|
|
|
|
def test_user_urls_are_fixed_or_dropped():
|
|
record = UserRecord.model_validate(
|
|
{**BASE, "username": "bob", "website": "example.com", "git_link": "not a url"}
|
|
)
|
|
assert record.website == "https://example.com"
|
|
assert record.git_link == ""
|
|
|
|
|
|
def test_user_bio_over_limit_is_rejected():
|
|
with pytest.raises(ValidationError):
|
|
UserRecord.model_validate({**BASE, "username": "bob", "bio": "x" * (LIMITS.max_bio_length + 1)})
|
|
|
|
|
|
def test_user_counters_and_credentials_are_not_fields():
|
|
known = UserRecord.known_fields()
|
|
for name in ("email", "password_hash", "role", "xp", "level", "stars", "api_key"):
|
|
assert name not in known
|
|
assert "stars" not in PostRecord.known_fields()
|
|
|
|
|
|
def test_envelope_normalizes_origin_and_bounds_batch():
|
|
form = SyncBatchForm(origin="Other.ORG", records=[{}])
|
|
assert form.origin == "other.org"
|
|
with pytest.raises(ValidationError):
|
|
SyncBatchForm(origin="other.org", records=[])
|
|
with pytest.raises(ValidationError):
|
|
SyncBatchForm(origin="other.org", records=[{}] * (LIMITS.max_batch_records + 1))
|
|
with pytest.raises(ValidationError):
|
|
SyncBatchForm(origin="bad origin", records=[{}])
|
|
|
|
|
|
def test_new_kinds_have_models_and_a_published_schema():
|
|
from devplacepy.services.sync.records import RECORD_MODELS
|
|
from devplacepy.services.sync.spec import KINDS
|
|
|
|
assert set(RECORD_MODELS) == set(KINDS)
|
|
for model in RECORD_MODELS.values():
|
|
assert model.model_json_schema()["title"]
|
|
|
|
|
|
def test_vote_value_and_target_kind_are_closed_sets():
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from devplacepy.services.sync.records import VoteRecord
|
|
|
|
base = {"origin_uid": "v", "voter_uid": "u", "target_kind": "post", "target_uid": "p", "value": 1, "created_at": "2025-01-01T00:00:00Z"}
|
|
assert VoteRecord.model_validate(base).value == 1
|
|
for change in ({"value": 0}, {"value": 2}, {"target_kind": "gist"}, {"voter_uid": "bad id"}):
|
|
with pytest.raises(ValidationError):
|
|
VoteRecord.model_validate({**base, **change})
|
|
|
|
|
|
def test_attachment_fingerprint_ignores_the_blob_but_not_its_hash():
|
|
import base64
|
|
|
|
from devplacepy.services.sync.records import AttachmentRecord
|
|
|
|
def make(data):
|
|
return AttachmentRecord.model_validate({
|
|
"origin_uid": "a", "author_uid": "u", "target_kind": "post", "target_uid": "p",
|
|
"filename": "dir/evil\\name.png", "content_b64": base64.b64encode(data).decode(), "created_at": "2025-01-01T00:00:00Z",
|
|
})
|
|
|
|
first, second = make(b"one"), make(b"two")
|
|
assert first.filename == "name.png"
|
|
assert "content_b64" not in first.content_fields() and first.content_fields()["sha256"] != second.content_fields()["sha256"]
|
|
assert first.content_bytes() == b"one"
|
|
|
|
|
|
def test_batch_byte_cap(monkeypatch):
|
|
import pytest
|
|
from dataclasses import replace
|
|
from pydantic import ValidationError
|
|
|
|
from devplacepy.services.sync import records
|
|
from devplacepy.services.sync.spec import LIMITS
|
|
|
|
monkeypatch.setattr(records, "LIMITS", replace(LIMITS, max_batch_bytes=100))
|
|
with pytest.raises(ValidationError):
|
|
records.SyncBatchForm(origin="a.b", records=[{"x": "y" * 200}])
|
|
|
|
|
|
def test_ack_form_validates_kinds_and_ids():
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from devplacepy.services.sync.records import SyncAckForm
|
|
|
|
assert SyncAckForm(origin="A.B", items='[{"kind": "posts", "origin_uid": "p1"}]').items == [{"kind": "posts", "origin_uid": "p1"}]
|
|
for items in ([], [{"kind": "gists", "origin_uid": "x"}], [{"kind": "posts", "origin_uid": ""}], "not json"):
|
|
with pytest.raises(ValidationError):
|
|
SyncAckForm(origin="a.b", items=items)
|