freezeTime(); $this->f = receiptFixture(); $this->originalStorage = app()->storagePath(); $this->temporaryStorage = $this->originalStorage.'/framework/testing/health-'.Str::uuid(); File::ensureDirectoryExists($this->temporaryStorage.'/framework'); app()->useStoragePath($this->temporaryStorage); auth()->guard()->logout(); }); afterEach(function () { app()->useStoragePath($this->originalStorage); File::deleteDirectory($this->temporaryStorage); }); test('replenishment health is read only and emits success without creating work purchases or telemetry', function () { app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0); $path = storage_path('framework/replenishment-command-status.json'); $beforeFile = File::get($path); $before = []; foreach (['requisitions', 'stock_movements', 'audit_logs', 'replenishment_work', 'supply_records'] as $table) { $before[$table] = DB::table($table)->orderBy('id')->get()->toArray(); } $result = app(ReplenishmentQueue::class)->health(); expect($result['ok'])->toBeTrue()->and($result['reasons'])->toBe([])->and($result['work'])->toBe(['total' => 0, 'pending' => 0, 'failed' => 0]); $this->artisan('sgc:replenishment-health', ['--json' => true])->expectsOutput(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))->assertExitCode(0); expect(auth()->check())->toBeFalse()->and(File::get($path))->toBe($beforeFile); foreach ($before as $table => $rows) { expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows); } }); test('replenishment health reports missing stale damaged or unsuccessful observation without certifying execution', function () { expect(app(ReplenishmentQueue::class)->health()['reasons'])->toContain('not_recent', 'last_run_not_successful'); File::put(storage_path('framework/replenishment-command-status.json'), '{broken'); expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeFalse(); File::put(storage_path('framework/replenishment-command-status.json'), json_encode(['result' => 'passed', 'finished_at' => now()->subHour()->toIso8601String(), 'installation' => app(ReplenishmentHeartbeat::class)->fingerprint()], JSON_THROW_ON_ERROR)); expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['not_recent']); app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 1); expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['last_run_not_successful']); $this->artisan('sgc:replenishment-health')->expectsOutput('Reposição: verificação técnica requer atenção.')->expectsOutput('Última passagem sem sucesso confirmado.')->assertExitCode(1); }); test('replenishment health aggregates pending failures while the workdesk stays tenant scoped and unchanged', function () { $queue = app(ReplenishmentQueue::class); $this->actingAs($this->f['receiver']); $first = $queue->enqueue($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id); $foreign = receiptFixture(); $this->actingAs($foreign['receiver']); $second = $queue->enqueue($foreign['receiver'], $foreign['stock']->id, $foreign['warehouse']->id); DB::table('replenishment_work')->where('id', $second)->update(['failures' => 1, 'last_error' => 'PRIVATE-WORK-DETAIL']); $before = DB::table('replenishment_work')->orderBy('id')->get()->toArray(); app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0); auth()->guard()->logout(); $result = $queue->health(); expect($result['ok'])->toBeFalse()->and($result['reasons'])->toBe(['pending_signals', 'failed_work']) ->and($result['work'])->toBe(['total' => 2, 'pending' => 2, 'failed' => 1]); expect(json_encode($result))->not->toContain('PRIVATE-WORK-DETAIL')->not->toContain('tenant_id')->not->toContain('responsible_id'); expect(DB::table('replenishment_work')->orderBy('id')->get()->toArray())->toEqual($before); $this->actingAs($this->f['receiver']); expect((int) $queue->status($this->f['receiver'])['work']->total)->toBe(1); expect(DB::table('replenishment_work')->where('id', $first)->value('processed_generation'))->toBe(0); $unassigned = User::factory()->create(['tenant_id' => null]); $this->actingAs($unassigned); expect((int) $queue->status($unassigned)['work']->total)->toBe(0); }); test('replenishment health rejects interactive identity and unavailable composition without processing work', function () { $this->actingAs($this->f['receiver']); try { app(ReplenishmentQueue::class)->health(); test()->fail('Interactive identity must not be used for technical health'); } catch (HttpException $error) { expect($error->getStatusCode())->toBe(403); } auth()->guard()->logout(); config(['work_context.composition' => 'sgf']); $result = app(ReplenishmentQueue::class)->health(); expect($result['ok'])->toBeFalse()->and($result['recovery_ready'])->toBeFalse()->and($result['reasons'])->toContain('not_installed')->and($result['work'])->toBeNull(); }); test('replenishment health command returns generic unavailability without exposing an exception', function () { $this->mock(ReplenishmentQueue::class)->shouldReceive('health')->once()->andThrow(new RuntimeException('PRIVATE-CONNECTION-DETAIL')); $this->artisan('sgc:replenishment-health', ['--json' => true])->expectsOutput(json_encode(['checked_at' => now()->toIso8601String(), 'ok' => false, 'reasons' => ['unavailable']], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))->assertExitCode(1); }); test('replenishment health requires a bound observation and rejects another installation without leaking its identity', function () { $path = storage_path('framework/replenishment-command-status.json'); File::put($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String()], JSON_THROW_ON_ERROR)); expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['unbound_observation']); File::put($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String(), 'installation' => str_repeat('a', 64)], JSON_THROW_ON_ERROR)); expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeFalse(); $this->actingAs($this->f['receiver']); expect(app(ReplenishmentQueue::class)->status($this->f['receiver'])['schedulerRecent'])->toBeFalse(); auth()->guard()->logout(); app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0); expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeTrue(); expect(json_encode(app(ReplenishmentQueue::class)->health()))->not->toContain(app(ReplenishmentHeartbeat::class)->fingerprint()); });