Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions loopx/event_sourced_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,13 +946,11 @@ def build_state_projection(
kind = event["event_type"]
edits = kind in (TODO_ADDED, TODO_UPDATED)
order = payload.get("planner_order") if kind == TODO_ADDED else None
if order is not None:
if isinstance(order, bool):
raise StateEventError("planner_order must be an integer")
try:
order = int(order)
except (ValueError, TypeError, OverflowError) as exc:
raise StateEventError("planner_order must be an integer") from exc
# The typed fold rejects non-integer orders, so the adapter must not
# coerce them first: truncating 1.5 to 1 would sort a Todo by one value
# and report another. Every producer of this payload writes an integer.
if order is not None and (isinstance(order, bool) or not isinstance(order, int)):
raise StateEventError("planner_order must be an integer")
sequence = event.get("append_sequence")
for value in (order, sequence):
if value is not None and abs(value) > 2**53 - 1:
Expand Down
26 changes: 26 additions & 0 deletions tests/control_plane/test_event_replay_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,29 @@ def measured(method, params, **kwargs):
# Duplicate identity protection must survive a batch boundary as well.
with pytest.raises(StateEventError, match="already exists"):
build_state_projection([*source, event("todo_added", 4101, title="Cannot reset history")])


def test_fractional_planner_order_is_rejected_before_typing():
# The adapter used to truncate 1.5 to 1, so the fold sorted by one value
# while the projection still reported the original fraction.
with pytest.raises(StateEventError, match="planner_order must be an integer"):
build_state_projection([
event("todo_added", 1, title="Fractional", planner_order=1.5),
event("todo_added", 2, title="Integer", planner_order=1, todo_id="todo_beta"),
])


@pytest.mark.parametrize("order", [True, "1", [1], {"value": 1}])
def test_non_integer_planner_order_forms_are_rejected(order):
with pytest.raises(StateEventError, match="planner_order must be an integer"):
build_state_projection([event("todo_added", 1, title="Bad order", planner_order=order)])


def test_integer_and_absent_planner_order_still_project():
result = build_state_projection([
event("todo_added", 1, title="First", planner_order=3),
event("todo_added", 2, title="Second", todo_id="todo_beta"),
])
items = result["agent_todos"]["items"]
assert [item["todo_id"] for item in items] == ["todo_alpha", "todo_beta"]
assert items[0]["planner_order"] == 3
Loading