Refactor(allocator): Extract cycle request builder

The functions that turn one entry's observed slots and reservations
into the allocator's cycle inputs (the observation, adoption,
allocation and rekey requests, the active identity set, planned slots
and the observed-slot matcher) made up nearly half of
code_allocation.py. Move them into cycle_requests.py so the allocation
step only orchestrates the cycle.

The observed-slot matcher becomes the public matched_code_slot, since
the rotation guard and divergence check receive it from the step.
build_cycle_observation is moved verbatim and stays unfiltered by
exclusions; the source-level test pinning that now reads the new
file. Importers in the allocator package and the tests now use
cycle_requests directly. Behaviour is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
This commit is contained in:
Andrew GrimbergandCopilot committed 2026-11-02 12:04:41 -08:00
1 parent 57b82a6792
commit e94a61543f
13 files changed
+310 -275

No files matched your search

@@ -137,7 +137,7 @@ def _forced_release_holds(allocator: DoorCodeAllocator) -> list[dict[str, Any]]:
def _collect_observations(allocator: DoorCodeAllocator) -> list[Any]:
"""Return current coordinator observations for diagnostics."""
from ..coordinator_helpers.code_allocation import build_cycle_observation
from ..coordinator_helpers.cycle_requests import build_cycle_observation
observations = []
for entry_id, entry_data in allocator.hass.data.get(DOMAIN, {}).items():
@@ -147,7 +147,7 @@ async def _guard_refusal(
hass: HomeAssistant, target: exclusion_targets.ExclusionTarget, slot: int
) -> str | None:
"""Return why this slot may not be excluded, or None when it may."""
from ..coordinator_helpers.code_allocation import build_cycle_observation
from ..coordinator_helpers.cycle_requests import build_cycle_observation
from .reissue_preview import observe_managed_slots_without_mutation
from .singleton import async_get_or_create_allocator
@@ -9,7 +9,7 @@ from dataclasses import replace
from typing import Any
from ..coordinator_helpers import adoption_gate
from ..coordinator_helpers import code_allocation
from ..coordinator_helpers import cycle_requests
from ..coordinator_helpers import keymaster_observation
from ..coordinator_helpers import physical_remap
from ..coordinator_helpers import reissue as coordinator_reissue
@@ -108,7 +108,7 @@ async def preview_request_and_result(
return request, ReissuePreview(reason="recovery_fail_closed")
if target.identity_key is not None and request.preferred_code is None:
return request, ReissuePreview(reason="no_existing_allocation")
observation = code_allocation.build_cycle_observation(
observation = cycle_requests.build_cycle_observation(
target.entry_id, target.lockname, observed_slots
)
excluded = frozenset(slot_exclusions.load(coordinator.hass, target.entry_id))
@@ -174,7 +174,7 @@ def _adoption_would_complete(
else frozenset(),
)
reservations = preview_reservations(coordinator, target, observed_slots)
adoptions = code_allocation.build_adoption_requests(
adoptions = cycle_requests.build_adoption_requests(
target.entry_id,
target.lockname,
coordinator.code_length,
@@ -100,7 +100,7 @@ def _loaded_entry_ids(hass: HomeAssistant) -> set[str]:
def _collect_observations(hass: HomeAssistant) -> list[CycleObservation]:
"""Return current observations from loaded Rental Control coordinators."""
from ..coordinator_helpers.code_allocation import build_cycle_observation
from ..coordinator_helpers.cycle_requests import build_cycle_observation
observations: list[CycleObservation] = []
for entry_id, entry_data in hass.data.get(DOMAIN, {}).items():
@@ -7,38 +7,28 @@ from __future__ import annotations
from dataclasses import replace
import logging
from typing import TYPE_CHECKING
import uuid
from homeassistant.components.persistent_notification import async_create
from homeassistant.util import dt
from ..allocator import code_origin
from ..allocator import entry_info
from ..allocator import get_allocator
from ..allocator.models import AdoptionRequest
from ..allocator.models import AllocationOrigin
from ..allocator.models import AllocationRequest
from ..allocator.models import AllocationResult
from ..allocator.models import CycleObservation
from ..allocator.models import CycleRequest
from ..allocator.models import CycleResult
from ..const import DOMAIN
from ..const import NAME
from ..reconciliation import SlotStatus
from ..reconciliation import compute_desired_plan
from ..reconciliation.desired import select_eligible_reservations
from . import adoption_gate
from . import cycle_requests
from . import generator_divergence
from . import reissue
from . import slot_exclusions
from . import unreadable_slots
from .models import ObservedSlotQuery
from .reissue import CodeResolutionRequest
from .reissue import CodeResolutionResult
from .reissue import ReissueSuppression
from .rotation_guard import RotationGuardTarget
from .rotation_guard import require_owned_code_for_rotation
from .slot_matching import find_observed_slot
if TYPE_CHECKING:
from ..reconciliation import ManagedSlot
@@ -46,10 +36,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
_EXHAUSTED_NOTIFICATION_ID = f"{DOMAIN}_code_registry_exhausted"
_ISSUED_ORIGINS = {
origin.value: origin
for origin in (AllocationOrigin.PREFERRED, AllocationOrigin.COLLISION_RESOLVED)
}
async def async_resolve_codes(
@@ -74,7 +60,9 @@ async def async_resolve_codes(
reissue.target_key(directive.identity_key, directive.lockname, directive.slot)
for directive in request.forced_reissues
)
observation = build_cycle_observation(entry_id, lockname, managed_slots)
observation = cycle_requests.build_cycle_observation(
entry_id, lockname, managed_slots
)
exclusions = slot_exclusions.revalidate_persisted(
hass, entry_id, lockname, observation
)
@@ -96,13 +84,13 @@ async def async_resolve_codes(
require_owned_code_for_rotation(
RotationGuardTarget(allocator, entry_id, lockname, managed_slots),
reservations,
_matched_code_slot,
cycle_requests.matched_code_slot,
)
code_origin.record_cycle_evidence(entry_id, reservations)
request = generator_divergence.rotate_stale_codes(
request, allocator, _matched_code_slot
request, allocator, cycle_requests.matched_code_slot
)
adoptions = build_adoption_requests(
adoptions = cycle_requests.build_adoption_requests(
entry_id,
lockname,
code_length,
@@ -120,7 +108,7 @@ async def async_resolve_codes(
adoption_gate.report(entry_id, lockname, gate_status)
adoption_complete = gate_status.complete
allocations = (
build_allocation_requests(
cycle_requests.build_allocation_requests(
entry_id, lockname, code_length, managed_slots, reservations
)
if adoption_complete
@@ -130,9 +118,9 @@ async def async_resolve_codes(
CycleRequest(
observation=observation,
adoptions=adoptions,
rekeys=build_rekey_requests(reservations),
rekeys=cycle_requests.build_rekey_requests(reservations),
allocations=allocations,
active_keys=_active_keys(reservations, lockname),
active_keys=cycle_requests.active_keys(reservations, lockname),
adoption_complete=adoption_complete,
forced_reissues=request.forced_reissues,
excluded_slots=request.excluded_slots,
@@ -186,14 +174,6 @@ async def async_resolve_codes(
)
def _active_keys(reservations: list[Reservation], lockname: str | None) -> set[str]:
"""Return the identities whose codes the allocator keeps this cycle."""
kept = select_eligible_reservations(
reservations, include_checked_out=lockname is None
)
return {reservation.identity_key for reservation in kept}
def _apply_adoption_results(
allocator: object,
entry_id: str,
@@ -238,228 +218,6 @@ def _apply_allocation_results(
return exhausted
def build_cycle_observation(
entry_id: str,
lockname: str | None,
managed_slots: list[ManagedSlot],
) -> CycleObservation:
"""Build the allocator's physical-state observation for this entry."""
managed = [slot for slot in managed_slots if slot.managed]
observed_codes: dict[str, int] = {}
for slot in managed:
if slot.actual_code is not None and slot.status is not SlotStatus.UNKNOWN:
observed_codes.setdefault(slot.actual_code, slot.slot)
return CycleObservation(
entry_id=entry_id,
lockname=lockname,
managed_slots=frozenset(slot.slot for slot in managed),
observed_codes=observed_codes,
unreadable_slots=frozenset(
slot.slot for slot in managed if slot.status is SlotStatus.UNKNOWN
),
)
def build_adoption_requests(
entry_id: str,
lockname: str | None,
code_length: int,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
suppression: ReissueSuppression | None = None,
) -> list[AdoptionRequest]:
"""Return adoption requests for reservations with readable physical codes."""
suppression = suppression or ReissueSuppression()
if lockname is None:
return []
consumed_slots: set[int] = set()
requests = reissue.AdoptionRequests()
for reservation in reservations:
slot = _matched_code_slot(reservation, managed_slots, consumed_slots)
if slot is None or slot.actual_code is None:
continue
if reservation.identity_key in suppression.identity_keys or (
slot.slot in suppression.slots and reservation.protected_active
):
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
if reservation.code_rotation_pending:
_LOGGER.info(
"Skipping adoption for %s in slot %d because its date-based "
"code must rotate for the updated booking window",
reservation.identity_key,
slot.slot,
)
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
if not slot.actual_code.isdecimal() or len(slot.actual_code) != code_length:
_LOGGER.warning(
"Skipping adoption for %s in slot %d because the observed code "
"does not match the configured code length",
reservation.identity_key,
slot.slot,
)
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
requests.append(
AdoptionRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
code=slot.actual_code,
code_length=code_length,
lockname=lockname,
slot=slot.slot,
fingerprint_history=frozenset(reservation.fingerprint_history),
)
)
return requests
def _reserve_observed_code(
requests: reissue.AdoptionRequests,
entry_id: str,
lockname: str,
reservation: Reservation,
slot: ManagedSlot,
) -> None:
"""Hold a suppressed slot's observed code against re-issue elsewhere.
A suppressed slot is exempt from adoption, so nothing records the code
it still holds. Without a reservation the allocator is free to hand
that same code to another booking, which would program a duplicate
code onto the lock. Non-decimal codes are skipped because the
allocator can never produce one, so they cannot collide.
"""
code = slot.actual_code
if not code or not code.isdecimal():
return
requests.observed_reservations.append(
AdoptionRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
code=code,
code_length=len(code),
lockname=lockname,
slot=slot.slot,
)
)
def build_allocation_requests(
entry_id: str,
lockname: str | None,
code_length: int,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
) -> list[AllocationRequest]:
"""Return deterministic allocation requests for selected reservations."""
planned_slots = _planned_slots(lockname, managed_slots, reservations)
requests: list[AllocationRequest] = []
for reservation in sorted(reservations, key=lambda item: item.identity_key):
if reservation.identity_key not in planned_slots:
continue
planned_slot = planned_slots.get(reservation.identity_key)
if lockname is not None and planned_slot is None:
continue
preferred = reservation.slot_code
if preferred is None and not reservation.published_once:
continue
requests.append(
AllocationRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
preferred_code=preferred or ("0" * code_length),
code_length=code_length,
fingerprint_history=frozenset(reservation.fingerprint_history),
previously_published=reservation.published_once,
lockname=lockname if planned_slot is not None else None,
slot=planned_slot,
preferred_code_known=preferred is not None,
issued_origin=_ISSUED_ORIGINS.get(reservation.issued_origin or ""),
)
)
return requests
def build_rekey_requests(reservations: list[Reservation]) -> list[tuple[str, str]]:
"""Return historical fingerprint moves for active reservations."""
requests: list[tuple[str, str]] = []
seen: set[tuple[str, str]] = set()
for reservation in sorted(reservations, key=lambda item: item.identity_key):
if reservation.code_rotation_pending:
continue
for historical_key in sorted(reservation.fingerprint_history):
item = (historical_key, reservation.identity_key)
if historical_key != reservation.identity_key and item not in seen:
requests.append(item)
seen.add(item)
return requests
def _planned_slots(
lockname: str | None,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
) -> dict[str, int | None]:
"""Return planned physical slots for lock-backed selected reservations."""
if lockname is None:
return {
reservation.identity_key: None
for reservation in select_eligible_reservations(
reservations, include_checked_out=True
)
}
if not managed_slots:
return {}
plan = compute_desired_plan(
reservations=reservations,
managed_slots=managed_slots,
max_events=len([slot for slot in managed_slots if slot.managed]),
plan_id=str(uuid.uuid4()),
generated_at=dt.now(),
)
return dict(plan.selected)
def _matched_code_slot(
reservation: Reservation,
managed_slots: list[ManagedSlot],
consumed_slots: set[int],
) -> ManagedSlot | None:
"""Return the readable coded slot that physically matches a reservation."""
for slot in managed_slots:
if (
slot.managed
and slot.persisted_identity_key == reservation.identity_key
and slot.actual_code
and slot.status is not SlotStatus.UNKNOWN
and slot.slot not in consumed_slots
):
consumed_slots.add(slot.slot)
return slot
matched = find_observed_slot(
ObservedSlotQuery(
managed_slots=managed_slots,
slot_name=reservation.slot_name,
display_slot_name=reservation.display_slot_name,
consumed_slots=consumed_slots,
desired_start=reservation.buffered_start,
desired_end=reservation.buffered_end,
event_prefix="",
)
)
if (
matched is not None
and matched.actual_code
and matched.status is not SlotStatus.UNKNOWN
):
return matched
return None
def _apply_result(
reservations: list[Reservation],
identity_key: str,
@@ -0,0 +1,273 @@
# SPDX-FileCopyrightText: 2026 Andrew Grimberg <tykeal@bardicgrove.org>
# SPDX-License-Identifier: Apache-2.0
"""Build the allocator cycle inputs from one entry's observed lock state."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import uuid
from homeassistant.util import dt
from ..allocator.models import AdoptionRequest
from ..allocator.models import AllocationOrigin
from ..allocator.models import AllocationRequest
from ..allocator.models import CycleObservation
from ..reconciliation import SlotStatus
from ..reconciliation import compute_desired_plan
from ..reconciliation.desired import select_eligible_reservations
from . import reissue
from .models import ObservedSlotQuery
from .reissue import ReissueSuppression
from .slot_matching import find_observed_slot
if TYPE_CHECKING:
from ..reconciliation import ManagedSlot
from ..reconciliation import Reservation
_LOGGER = logging.getLogger(__name__)
_ISSUED_ORIGINS = {
origin.value: origin
for origin in (AllocationOrigin.PREFERRED, AllocationOrigin.COLLISION_RESOLVED)
}
def active_keys(reservations: list[Reservation], lockname: str | None) -> set[str]:
"""Return the identities whose codes the allocator keeps this cycle."""
kept = select_eligible_reservations(
reservations, include_checked_out=lockname is None
)
return {reservation.identity_key for reservation in kept}
def build_cycle_observation(
entry_id: str,
lockname: str | None,
managed_slots: list[ManagedSlot],
) -> CycleObservation:
"""Build the allocator's physical-state observation for this entry."""
managed = [slot for slot in managed_slots if slot.managed]
observed_codes: dict[str, int] = {}
for slot in managed:
if slot.actual_code is not None and slot.status is not SlotStatus.UNKNOWN:
observed_codes.setdefault(slot.actual_code, slot.slot)
return CycleObservation(
entry_id=entry_id,
lockname=lockname,
managed_slots=frozenset(slot.slot for slot in managed),
observed_codes=observed_codes,
unreadable_slots=frozenset(
slot.slot for slot in managed if slot.status is SlotStatus.UNKNOWN
),
)
def build_adoption_requests(
entry_id: str,
lockname: str | None,
code_length: int,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
suppression: ReissueSuppression | None = None,
) -> list[AdoptionRequest]:
"""Return adoption requests for reservations with readable physical codes."""
suppression = suppression or ReissueSuppression()
if lockname is None:
return []
consumed_slots: set[int] = set()
requests = reissue.AdoptionRequests()
for reservation in reservations:
slot = matched_code_slot(reservation, managed_slots, consumed_slots)
if slot is None or slot.actual_code is None:
continue
if reservation.identity_key in suppression.identity_keys or (
slot.slot in suppression.slots and reservation.protected_active
):
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
if reservation.code_rotation_pending:
_LOGGER.info(
"Skipping adoption for %s in slot %d because its date-based "
"code must rotate for the updated booking window",
reservation.identity_key,
slot.slot,
)
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
if not slot.actual_code.isdecimal() or len(slot.actual_code) != code_length:
_LOGGER.warning(
"Skipping adoption for %s in slot %d because the observed code "
"does not match the configured code length",
reservation.identity_key,
slot.slot,
)
requests.suppressed_slots.add(slot.slot)
_reserve_observed_code(requests, entry_id, lockname, reservation, slot)
continue
requests.append(
AdoptionRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
code=slot.actual_code,
code_length=code_length,
lockname=lockname,
slot=slot.slot,
fingerprint_history=frozenset(reservation.fingerprint_history),
)
)
return requests
def _reserve_observed_code(
requests: reissue.AdoptionRequests,
entry_id: str,
lockname: str,
reservation: Reservation,
slot: ManagedSlot,
) -> None:
"""Hold a suppressed slot's observed code against re-issue elsewhere.
A suppressed slot is exempt from adoption, so nothing records the code
it still holds. Without a reservation the allocator is free to hand
that same code to another booking, which would program a duplicate
code onto the lock. Non-decimal codes are skipped because the
allocator can never produce one, so they cannot collide.
"""
code = slot.actual_code
if not code or not code.isdecimal():
return
requests.observed_reservations.append(
AdoptionRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
code=code,
code_length=len(code),
lockname=lockname,
slot=slot.slot,
)
)
def build_allocation_requests(
entry_id: str,
lockname: str | None,
code_length: int,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
) -> list[AllocationRequest]:
"""Return deterministic allocation requests for selected reservations."""
planned_slots = _planned_slots(lockname, managed_slots, reservations)
requests: list[AllocationRequest] = []
for reservation in sorted(reservations, key=lambda item: item.identity_key):
if reservation.identity_key not in planned_slots:
continue
planned_slot = planned_slots.get(reservation.identity_key)
if lockname is not None and planned_slot is None:
continue
preferred = reservation.slot_code
if preferred is None and not reservation.published_once:
continue
requests.append(
AllocationRequest(
entry_id=entry_id,
identity_key=reservation.identity_key,
preferred_code=preferred or ("0" * code_length),
code_length=code_length,
fingerprint_history=frozenset(reservation.fingerprint_history),
previously_published=reservation.published_once,
lockname=lockname if planned_slot is not None else None,
slot=planned_slot,
preferred_code_known=preferred is not None,
issued_origin=_ISSUED_ORIGINS.get(reservation.issued_origin or ""),
)
)
return requests
def build_rekey_requests(reservations: list[Reservation]) -> list[tuple[str, str]]:
"""Return historical fingerprint moves for active reservations."""
requests: list[tuple[str, str]] = []
seen: set[tuple[str, str]] = set()
for reservation in sorted(reservations, key=lambda item: item.identity_key):
if reservation.code_rotation_pending:
continue
for historical_key in sorted(reservation.fingerprint_history):
item = (historical_key, reservation.identity_key)
if historical_key != reservation.identity_key and item not in seen:
requests.append(item)
seen.add(item)
return requests
def _planned_slots(
lockname: str | None,
managed_slots: list[ManagedSlot],
reservations: list[Reservation],
) -> dict[str, int | None]:
"""Return planned physical slots for lock-backed selected reservations."""
if lockname is None:
return {
reservation.identity_key: None
for reservation in select_eligible_reservations(
reservations, include_checked_out=True
)
}
if not managed_slots:
return {}
plan = compute_desired_plan(
reservations=reservations,
managed_slots=managed_slots,
max_events=len([slot for slot in managed_slots if slot.managed]),
plan_id=str(uuid.uuid4()),
generated_at=dt.now(),
)
return dict(plan.selected)
def matched_code_slot(
reservation: Reservation,
managed_slots: list[ManagedSlot],
consumed_slots: set[int],
) -> ManagedSlot | None:
"""Return the readable coded slot that physically matches a reservation."""
for slot in managed_slots:
if (
slot.managed
and slot.persisted_identity_key == reservation.identity_key
and slot.actual_code
and slot.status is not SlotStatus.UNKNOWN
and slot.slot not in consumed_slots
):
consumed_slots.add(slot.slot)
return slot
matched = find_observed_slot(
ObservedSlotQuery(
managed_slots=managed_slots,
slot_name=reservation.slot_name,
display_slot_name=reservation.display_slot_name,
consumed_slots=consumed_slots,
desired_start=reservation.buffered_start,
desired_end=reservation.buffered_end,
event_prefix="",
)
)
if (
matched is not None
and matched.actual_code
and matched.status is not SlotStatus.UNKNOWN
):
return matched
return None
__all__ = [
"active_keys",
"build_adoption_requests",
"build_allocation_requests",
"build_cycle_observation",
"build_rekey_requests",
"matched_code_slot",
]
@@ -16,6 +16,7 @@ import pytest
from custom_components.rental_control.allocator.allocator import DoorCodeAllocator
from custom_components.rental_control.allocator.models import AllocationOrigin
from custom_components.rental_control.allocator.models import AllocationRequest
from custom_components.rental_control.allocator.store import RegistryStore
from custom_components.rental_control.coordinator_helpers import code_allocation
from custom_components.rental_control.reconciliation import ManagedSlot
@@ -82,7 +83,7 @@ async def test_full_code_space_is_shared_not_partitioned() -> None:
"""Every entry can use any code not already held by another identity."""
allocator = _allocator()
first = await allocator.async_allocate(
code_allocation.AllocationRequest(
AllocationRequest(
entry_id="entry-a",
identity_key="identity-a",
preferred_code="1234",
@@ -90,7 +91,7 @@ async def test_full_code_space_is_shared_not_partitioned() -> None:
)
)
second = await allocator.async_allocate(
code_allocation.AllocationRequest(
AllocationRequest(
entry_id="entry-b",
identity_key="identity-b",
preferred_code="5678",
+3 -2
View File
@@ -24,6 +24,7 @@ from custom_components.rental_control.allocator.models import CycleObservation
from custom_components.rental_control.allocator.models import CycleRequest
from custom_components.rental_control.const import DOMAIN
from custom_components.rental_control.coordinator_helpers import code_allocation
from custom_components.rental_control.coordinator_helpers import cycle_requests
from custom_components.rental_control.coordinator_helpers import slot_notifications
from custom_components.rental_control.coordinator_helpers import unreadable_slots
from custom_components.rental_control.coordinator_helpers.coordinator_refresh_shell import (
@@ -257,7 +258,7 @@ def test_rekey_requests_are_deterministic() -> None:
first = _reservation("identity-a", code="1111")
first.fingerprint_history = {"old-a2", "old-a1", "identity-a"}
requests = code_allocation.build_rekey_requests([second, first])
requests = cycle_requests.build_rekey_requests([second, first])
assert requests == [
("old-a1", "identity-a"),
@@ -577,7 +578,7 @@ def test_invalid_code_slots_are_recorded_as_suppressed() -> None:
for observed in ("22222", "12AB"):
requests = cast(
AdoptionRequests,
code_allocation.build_adoption_requests(
cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
+3 -3
View File
@@ -39,7 +39,7 @@ def _isolate_preview(monkeypatch: pytest.MonkeyPatch) -> None:
reissue_preview, "build_preview_request", lambda *_a, **_k: "request"
)
monkeypatch.setattr(
reissue_preview.code_allocation,
reissue_preview.cycle_requests,
"build_cycle_observation",
lambda entry_id, lockname, _slots: CycleObservation(
entry_id=entry_id,
@@ -160,7 +160,7 @@ async def test_preview_forwards_exclusions_to_the_adoption_check(
reissue_preview, "build_preview_request", lambda *_a, **_k: "request"
)
monkeypatch.setattr(
reissue_preview.code_allocation,
reissue_preview.cycle_requests,
"build_cycle_observation",
lambda entry_id, lockname, _slots: CycleObservation(
entry_id=entry_id,
@@ -171,7 +171,7 @@ async def test_preview_forwards_exclusions_to_the_adoption_check(
),
)
monkeypatch.setattr(
reissue_preview.code_allocation, "build_adoption_requests", lambda *_a: []
reissue_preview.cycle_requests, "build_adoption_requests", lambda *_a: []
)
monkeypatch.setattr(reissue_preview, "preview_reservations", lambda *_a: [])
monkeypatch.setattr(
@@ -108,7 +108,7 @@ async def test_orphan_cleanup_retains_an_excluded_slots_code() -> None:
def test_observation_builder_knows_nothing_of_exclusions() -> None:
"""The shared observation must never be filtered by exclusions."""
source = Path(
"custom_components/rental_control/coordinator_helpers/code_allocation.py"
"custom_components/rental_control/coordinator_helpers/cycle_requests.py"
).read_text(encoding="utf-8")
builder = source.split("def build_cycle_observation(", 1)[1].split("\ndef ", 1)[0]
+4 -3
View File
@@ -16,6 +16,7 @@ import pytest
from custom_components.rental_control.allocator import allocator as allocator_module
from custom_components.rental_control.allocator.allocator import DoorCodeAllocator
from custom_components.rental_control.coordinator_helpers import code_allocation
from custom_components.rental_control.coordinator_helpers import cycle_requests
from custom_components.rental_control.coordinator_helpers import slot_notifications
from custom_components.rental_control.coordinator_helpers.reissue import (
AdoptionRequests,
@@ -161,7 +162,7 @@ def test_rotation_pending_slot_reserves_its_observed_code() -> None:
requests = cast(
AdoptionRequests,
code_allocation.build_adoption_requests(
cycle_requests.build_adoption_requests(
"entry-a", "front", 4, [target_slot], [target]
),
)
@@ -178,7 +179,7 @@ def test_non_decimal_invalid_code_slot_reserves_nothing() -> None:
"""A non-decimal observed code cannot collide, so it reserves nothing."""
requests = cast(
AdoptionRequests,
code_allocation.build_adoption_requests(
cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
@@ -200,7 +201,7 @@ def test_wrong_length_invalid_code_slot_is_reserved() -> None:
"""
requests = cast(
AdoptionRequests,
code_allocation.build_adoption_requests(
cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
+5 -4
View File
@@ -21,6 +21,7 @@ from custom_components.rental_control.allocator.models import AllocationOwner
from custom_components.rental_control.allocator.models import AllocationRecord
from custom_components.rental_control.allocator.reissue import forced_release_hold_key
from custom_components.rental_control.coordinator_helpers import code_allocation
from custom_components.rental_control.coordinator_helpers import cycle_requests
from custom_components.rental_control.coordinator_helpers import reissue
from custom_components.rental_control.coordinator_helpers.checkin_protection import (
build_protected_reservation,
@@ -121,7 +122,7 @@ def test_adoption_requests_skip_only_suppressed_target() -> None:
"""Suppression skips the target's adoption and leaves peers unchanged."""
first = _reservation("identity-a", "Target Guest", "1234")
second = _reservation("identity-b", "Other Guest", "5678")
requests = code_allocation.build_adoption_requests(
requests = cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
@@ -208,7 +209,7 @@ def test_slot_suppression_skips_protected_adoption_only() -> None:
protected = _reservation("identity-b", "Target Guest", "1234")
protected.protected_active = True
live_requests = code_allocation.build_adoption_requests(
live_requests = cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
@@ -216,7 +217,7 @@ def test_slot_suppression_skips_protected_adoption_only() -> None:
[live],
reissue.ReissueSuppression(frozenset(), frozenset({1})),
)
protected_requests = code_allocation.build_adoption_requests(
protected_requests = cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
@@ -233,7 +234,7 @@ def test_suppressed_target_consumes_slot_before_peer_fallback() -> None:
"""A peer cannot adopt the suppressed target's old physical code."""
first = _reservation("identity-a", "Same Guest", "1234")
second = _reservation("identity-b", "Same Guest", "5678")
requests = code_allocation.build_adoption_requests(
requests = cycle_requests.build_adoption_requests(
"entry-a",
"front",
4,
+2 -2
View File
@@ -36,10 +36,10 @@ from freezegun import freeze_time
import pytest
from custom_components.rental_control.codegen import generate_slot_code
from custom_components.rental_control.coordinator_helpers.code_allocation import (
from custom_components.rental_control.coordinator_helpers.cycle_requests import (
build_adoption_requests,
)
from custom_components.rental_control.coordinator_helpers.code_allocation import (
from custom_components.rental_control.coordinator_helpers.cycle_requests import (
build_rekey_requests,
)
from custom_components.rental_control.coordinator_helpers.models import (