fix: include rendered_at in 304 cache check so re-renders invalidate
CI / test (push) Has been cancelled

After re-cropping an image, the renderer regenerates the .bin and
advances the asset's rendered_at, but the device's 304 short-circuit
still matched on (image_id, orientation) only — so the device kept
serving the old upside-down/stale bytes from its local cache despite
the server having freshly-rendered correct ones.

Adds device.current_rendered_at, populated whenever a 200 response is
served, and tightens the 304 condition to require all three (image id,
orientation, rendered_at) to match. The asset lookup now happens before
the 304 check so its rendered_at is in scope for the comparison.

No firmware change — this is server-side cache logic. Existing devices
get null current_rendered_at after the migration; their next poll falls
through 304 and re-fetches once, then the cache is in sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:16:48 -04:00
parent 4586079fae
commit b700a4a018
4 changed files with 101 additions and 23 deletions
@@ -66,7 +66,8 @@ class DeviceImageControllerTest extends AppWebTestCase
->setDeviceModel(DeviceModel::V1)
->setOrientation(Orientation::Landscape)
->setStatus(RenderStatus::Ready)
->setFilePath(self::BIN_PATH);
->setFilePath(self::BIN_PATH)
->setRenderedAt(new \DateTimeImmutable());
$this->em()->persist($asset);
}
@@ -166,10 +167,13 @@ class DeviceImageControllerTest extends AppWebTestCase
$image = $setup['image'];
$imageId = $image->getId();
$device->setLockedImage($image);
// Simulate the device having already received this image at the current
// orientation — the 304 path now requires this to match too.
// orientation and rendered_at — the 304 path now requires all three
// (image id, orientation, rendered_at) to match.
$asset = $this->em()->getRepository(RenderedAsset::class)->findOneBy(['image' => $image]);
$device->setLockedImage($image);
$device->setCurrentImageOrientation(Orientation::Landscape);
$device->setCurrentRenderedAt($asset->getRenderedAt());
$this->em()->flush();
$this->client->request('GET', '/api/device/' . self::MAC . '/image', [], [], [
@@ -211,6 +215,34 @@ class DeviceImageControllerTest extends AppWebTestCase
$this->assertResponseStatusCodeSame(200);
}
public function test_re_render_returns_200_even_when_image_id_and_orientation_match(): void
{
$setup = $this->createTestSetup();
$deviceId = $setup['device']->getId();
$imageId = $setup['image']->getId();
// Seed: device receives the image once, server stores currentRenderedAt.
$this->client->request('GET', '/api/device/' . self::MAC . '/image');
$this->assertResponseStatusCodeSame(200);
// Simulate a re-render: the asset's rendered_at advances (e.g. user
// re-cropped the image, RenderImageMessageHandler ran again).
$this->em()->clear();
$asset = $this->em()->getRepository(RenderedAsset::class)->findOneBy([
'image' => $this->em()->find(Image::class, $imageId),
'orientation' => Orientation::Landscape,
]);
$asset->setRenderedAt(new \DateTimeImmutable('+1 minute'));
$this->em()->flush();
// Same image id, same orientation — but the bytes have changed, so
// the 304 cache must invalidate and the device must re-fetch.
$this->client->request('GET', '/api/device/' . self::MAC . '/image', [], [], [
'HTTP_X-Current-Image-Id' => (string) $imageId,
]);
$this->assertResponseStatusCodeSame(200);
}
public function test_poll_advances_current_image(): void
{
$setup = $this->createTestSetup();