Files
compras/tests/Feature/ReplenishmentHeartbeatTest.php
T
2026-11-03 11:51:44 +02:00

107 lines
5.2 KiB
PHP

<?php
use App\Services\ReplenishmentProcessLock;
use App\Services\ReplenishmentQueue;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
beforeEach(function () {
$this->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();
});