fix(rotation): gate poll-driven advance() behind isDue() check
CI / test (push) Has been cancelled

Symptom: with wakeTimes=[4 AM, 9 PM, 9:15 PM], the frame rotated to a
fresh photo at 10:14 AM when the device was reconnected and polled.
The wakeTimes only governed *when* the device polled (via X-Interval-Ms);
they didn't gate whether the server picked a new image when it did.
Power-on or button-press polls would always rotate.

Fix: move the existing isDue() logic from AdvanceRotationMessageHandler
into RotationService as a public method, and gate
DeviceImageController::image so off-schedule polls return the device's
current image (which 304s when X-Current-Image-Id matches) rather than
calling advance(). The scheduler-driven handler still uses the same
isDue — both code paths now share one source of truth.

Tests:
  - DeviceImageControllerTest: new test asserting an off-schedule poll
    returns 304 without rotating; existing wakeTimes tests reworked to
    use slot lists that always have a past slot regardless of run time.
  - AdvanceRotationMessageHandlerTest: existing AR-04 through AR-07
    keep covering isDue's semantics — they now go through the service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 12:14:32 -04:00
parent bf9d4ebc58
commit 5b3e2e47d7
4 changed files with 118 additions and 63 deletions
+12 -2
View File
@@ -83,8 +83,18 @@ class DeviceImageController extends AbstractController
// the second flush triggers a second publish to keep it accurate.
$this->mercure->publishDevice((int) $device->getId(), $this->serializer->serialize($device));
// Locked image bypasses rotation entirely.
$image = $device->getLockedImage() ?? $this->rotation->advance($device);
// Locked image bypasses rotation entirely. Otherwise, only advance the
// rotation when the device's configured schedule says it's due —
// off-schedule polls (boot, button-mash, scheduler-driven status check)
// hand back the device's current image without rotating, so the user
// doesn't see surprise refreshes between configured wake times.
if ($device->getLockedImage() !== null) {
$image = $device->getLockedImage();
} elseif ($this->rotation->isDue($device)) {
$image = $this->rotation->advance($device);
} else {
$image = $device->getCurrentImage();
}
if ($image === null) {
$this->logger->info('device.poll.no_image', [