once()->andReturn([0, ['stale-1', 'live-1']]); Redis::shouldReceive('pipeline')->once()->andReturnUsing(function ($cb) { $pipe = Mockery::mock(); $pipe->shouldReceive('exists')->twice(); $cb($pipe); return [0, 1]; }); Redis::shouldReceive('eval')->once()->andReturn(1); $result = app(PresenceIndexPruner::class)->prune(200, 1, 10, 0, false); expect($result['scanned'])->toBe(2) ->and($result['stale'])->toBe(1) ->and($result['live'])->toBe(1) ->and($result['removed'])->toBe(1); }); it('dry-run counts stale members without srem/eval', function () { Redis::shouldReceive('sscan')->once()->andReturn([0, ['stale-1']]); Redis::shouldReceive('pipeline')->once()->andReturnUsing(function ($cb) { $pipe = Mockery::mock(); $pipe->shouldReceive('exists')->once(); $cb($pipe); return [0]; }); Redis::shouldReceive('eval')->never(); $result = app(PresenceIndexPruner::class)->prune(200, 5, 100, 0, true); expect($result['stale'])->toBe(1) ->and($result['removed'])->toBe(0) ->and($result['dry_run'])->toBeTrue(); }); it('respects max-batches and max-members', function () { Redis::shouldReceive('sscan')->once()->andReturn([1, ['a', 'b']]); Redis::shouldReceive('pipeline')->once()->andReturnUsing(function ($cb) { $pipe = Mockery::mock(); $pipe->shouldReceive('exists')->twice(); $cb($pipe); return [0, 0]; }); Redis::shouldReceive('eval')->never(); $result = app(PresenceIndexPruner::class)->prune(200, 1, 2, 0, true); expect($result['batches'])->toBe(1) ->and($result['scanned'])->toBe(2); }); it('does not use smembers on the presence index', function () { $source = file_get_contents(base_path('app/Services/Traffic/PresenceIndexPruner.php')); expect(strtolower((string) $source))->not->toContain('smembers') ->and(strtolower((string) $source))->toContain('sscan'); }); it('never deletes the presence index key', function () { $source = (string) file_get_contents(base_path('app/Services/Traffic/PresenceIndexPruner.php')); expect($source)->toContain('SREM') ->and($source)->not->toContain('unlink') ->and($source)->not->toContain('->del(') ->and($source)->not->toContain('FLUSH'); });