fix: subset selection validation and remove unnecessary hash computation

Two bugs fixed in animal selection with checkboxes:

1. Confirmation dialog showed wrong count (e.g., "35 animals" instead of
   "2 animals" when only 2 were selected). Fixed by using valid_selected
   count in diff.server_count instead of full filter resolution count.

2. Spurious "Selection Changed" dialogs due to race condition in async
   hash computation. Fixed by removing client-side hash computation
   entirely - it was unnecessary since the server validates selected_ids
   directly against the filter resolution.

Changes:
- validation.py: Remove hash comparison in _validate_subset(), validate
  IDs directly, fix server_count in diff
- animal_select.py: Remove computeSelectionHash(), hidden roster_hash
  field, and related async fetch code
- test_selection_validation.py: Add tests for subset mode validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-02 13:01:07 +00:00
parent cccd76a44c
commit 628d5cc6e6
3 changed files with 101 additions and 75 deletions

View File

@@ -456,3 +456,86 @@ class TestSelectionMismatchError:
assert error.result is result
assert error.result.diff is diff
# ============================================================================
# Tests for validate_selection - subset mode
# ============================================================================
class TestValidateSelectionSubsetMode:
"""Tests for validate_selection with subset_mode=True."""
def test_subset_mode_returns_valid_when_all_selected_match(
self, seeded_db, animal_service, strip1_location_id
):
"""validate_selection returns valid=True when all selected IDs are in filter."""
# Create cohort of 5 animals
ts_utc = int(time.time() * 1000)
payload = make_cohort_payload(strip1_location_id, count=5)
event = animal_service.create_cohort(payload, ts_utc, "test_user")
all_ids = event.entity_refs["animal_ids"]
# User selects only 2 of them
selected_ids = all_ids[:2]
subset_hash = compute_roster_hash(selected_ids, None)
ctx = SelectionContext(
filter="species:duck",
resolved_ids=all_ids, # Full filter resolution
roster_hash=subset_hash, # Hash of selected subset
ts_utc=ts_utc,
from_location_id=None,
subset_mode=True,
selected_ids=selected_ids,
)
result = validate_selection(seeded_db, ctx)
assert result.valid is True
assert result.resolved_ids == selected_ids
assert result.diff is None
def test_subset_mode_diff_server_count_is_valid_selected_count(
self, seeded_db, animal_service, strip1_location_id, strip2_location_id
):
"""In subset mode, diff.server_count should be count of valid selected IDs, not full filter."""
# Create cohort of 5 animals
ts_create = int(time.time() * 1000)
payload = make_cohort_payload(strip1_location_id, count=5)
event = animal_service.create_cohort(payload, ts_create, "test_user")
all_ids = event.entity_refs["animal_ids"]
# User selects 2 animals
selected_ids = all_ids[:2]
subset_hash = compute_roster_hash(selected_ids, None)
# Move one selected animal away (makes it invalid for the filter)
ts_move = ts_create + 1000
move_payload = AnimalMovedPayload(
resolved_ids=[selected_ids[0]],
from_location_id=strip1_location_id,
to_location_id=strip2_location_id,
)
animal_service.move_animals(move_payload, ts_move, "test_user")
# Now validate at ts_move - one of the selected animals is no longer at Strip 1
ctx = SelectionContext(
filter="location:'Strip 1'",
resolved_ids=all_ids, # Full filter resolution at creation time
roster_hash=subset_hash,
ts_utc=ts_move, # Validate at move time
from_location_id=None,
subset_mode=True,
selected_ids=selected_ids,
)
result = validate_selection(seeded_db, ctx)
assert result.valid is False
assert result.diff is not None
# BUG: diff.server_count is currently len(resolved_ids) = 4 (5 minus moved)
# SHOULD BE: len(valid_selected) = 1 (2 selected minus 1 moved)
assert result.diff.server_count == 1 # Only 1 valid selected animal remains
assert result.diff.client_count == 2 # User selected 2
assert selected_ids[0] in result.diff.removed # The moved animal is invalid