freezeTime(); $this->originalStorage = app()->storagePath(); $this->heartbeatStorage = $this->originalStorage.'/framework/testing/heartbeat-'.Str::uuid(); File::ensureDirectoryExists($this->heartbeatStorage.'/framework'); app()->useStoragePath($this->heartbeatStorage); }); afterEach(function () { app()->useStoragePath($this->originalStorage); File::deleteDirectory($this->heartbeatStorage); }); test('native replenishment command records success or pending recovery without identities', function (int $failed, int $code, string $result) { $queue = $this->mock(ReplenishmentQueue::class); $queue->shouldReceive('ready')->once()->andReturnTrue(); $queue->shouldReceive('discover')->once(); $queue->shouldReceive('drain')->with(12)->once()->andReturn(['processed' => 2, 'failed' => $failed]); $this->artisan('sgc:replenishment-tick', ['--limit' => 12])->assertExitCode($code); $data = json_decode(File::get(storage_path('framework/replenishment-command-status.json')), true, 32, JSON_THROW_ON_ERROR); expect($data)->toHaveCount(6)->and($data['result'])->toBe($result)->and($data['exit_code'])->toBe($code) ->and($data['finished_at'])->toBe(now()->toIso8601String()); })->with([[0, 0, 'passed'], [1, 1, 'retry_pending']]); test('interrupted discovery replaces a previous success with recovery pending and preserves the original error', function () { File::put(storage_path('framework/replenishment-command-status.json'), '{"result":"passed"}'); $queue = $this->mock(ReplenishmentQueue::class); $queue->shouldReceive('ready')->once()->andReturnTrue(); $queue->shouldReceive('discover')->once()->andThrow(new RuntimeException('Synthetic discovery interruption')); $queue->shouldNotReceive('drain'); try { $this->artisan('sgc:replenishment-tick')->run(); test()->fail('Expected discovery failure'); } catch (RuntimeException $error) { expect($error->getMessage())->toBe('Synthetic discovery interruption'); } $data = json_decode(File::get(storage_path('framework/replenishment-command-status.json')), true, 32, JSON_THROW_ON_ERROR); expect($data['result'])->toBe('retry_pending')->and($data['exit_code'])->toBe(1); expect(File::get(storage_path('framework/replenishment-command-status.json')))->not->toContain('Synthetic discovery interruption'); }); test('invalid limits do not publish a successful scheduler observation', function () { $queue = $this->mock(ReplenishmentQueue::class); $queue->shouldReceive('ready')->once()->andReturnTrue(); $queue->shouldNotReceive('discover'); $queue->shouldNotReceive('drain'); $this->artisan('sgc:replenishment-tick', ['--limit' => 501])->assertExitCode(2); expect(File::exists(storage_path('framework/replenishment-command-status.json')))->toBeFalse(); }); test('overlapping native command neither processes work nor replaces the last completed observation', function () { $held = new ReplenishmentProcessLock; expect($held->acquire())->toBeTrue(); $path = storage_path('framework/replenishment-command-status.json'); File::put($path, '{"result":"retry_pending","finished_at":"previous"}'); $before = File::get($path); $queue = $this->mock(ReplenishmentQueue::class); $queue->shouldReceive('ready')->once()->andReturnTrue(); $queue->shouldNotReceive('discover'); $queue->shouldNotReceive('drain'); try { $this->artisan('sgc:replenishment-tick')->expectsOutput('Verificação periódica já em execução; aguarde a próxima passagem.')->assertExitCode(1); expect(File::get($path))->toBe($before); } finally { $held->release(); } $next = new ReplenishmentProcessLock; expect($next->acquire())->toBeTrue(); $next->release(); }); test('exception and successful runs both release native process exclusion', function () { $queue = $this->mock(ReplenishmentQueue::class); $queue->shouldReceive('ready')->twice()->andReturnTrue(); $queue->shouldReceive('discover')->once()->andThrow(new RuntimeException('Synthetic interruption')); try { $this->artisan('sgc:replenishment-tick')->run(); test()->fail('Expected interruption'); } catch (RuntimeException $error) { expect($error->getMessage())->toBe('Synthetic interruption'); } $queue->shouldReceive('discover')->once(); $queue->shouldReceive('drain')->once()->andReturn(['processed' => 0, 'failed' => 0]); $this->artisan('sgc:replenishment-tick')->assertExitCode(0); $next = new ReplenishmentProcessLock; expect($next->acquire())->toBeTrue(); $next->release(); }); test('destroyed worker lock can be acquired without deleting its control file', function () { $first = new ReplenishmentProcessLock; expect($first->acquire())->toBeTrue(); $contender = new ReplenishmentProcessLock; expect($contender->acquire())->toBeFalse(); unset($first); expect(File::exists(storage_path('framework/replenishment-command.lock')))->toBeTrue(); expect($contender->acquire())->toBeTrue(); $contender->release(); });