diff --git a/app/Http/Controllers/Api/SentinelController.php b/app/Http/Controllers/Api/SentinelController.php index ca47e9f9d..f76c58c0f 100644 --- a/app/Http/Controllers/Api/SentinelController.php +++ b/app/Http/Controllers/Api/SentinelController.php @@ -79,7 +79,7 @@ class SentinelController extends Controller return response()->json(['message' => 'Unauthorized'], 401); } $validator = Validator::make($request->all(), [ - 'containers' => ['required', 'array', 'min:1'], + 'containers' => ['present', 'array'], ]); if ($validator->fails()) { diff --git a/tests/Feature/SentinelPushDeduplicationTest.php b/tests/Feature/SentinelPushDeduplicationTest.php index 9d20851ed..b61e9933c 100644 --- a/tests/Feature/SentinelPushDeduplicationTest.php +++ b/tests/Feature/SentinelPushDeduplicationTest.php @@ -71,6 +71,15 @@ it('updates the heartbeat even when the job is skipped', function () use ($runni expect(Carbon::parse($this->server->fresh()->sentinel_updated_at)->diffInSeconds(now()))->toBeLessThan(5); }); +it('accepts an empty container list as a heartbeat when no containers are running', function () { + $this->server->update(['sentinel_updated_at' => now()->subHour()]); + + pushSentinel($this->token, sentinelPayload([]))->assertOk(); + + Queue::assertPushed(PushServerUpdateJob::class, 1); + expect(Carbon::parse($this->server->fresh()->sentinel_updated_at)->diffInSeconds(now()))->toBeLessThan(5); +}); + it('rejects malformed sentinel payloads before touching server state', function (array $payload) { $this->server->update(['sentinel_updated_at' => now()->subHour()]); $originalHeartbeat = $this->server->fresh()->sentinel_updated_at; @@ -87,7 +96,6 @@ it('rejects malformed sentinel payloads before touching server state', function })->with([ 'missing containers' => [[]], 'non-array containers' => [['containers' => 'not-an-array']], - 'empty containers' => [['containers' => []]], ]); it('guards the dedupe decision with a server scoped atomic cache lock', function () {