toBeFalse(); }); test('the migration removes old inactive and absent players while keeping present players', function () { Schema::table('players', function (Blueprint $table): void { $table->timestamp('inactive_at')->nullable(); }); $room = Room::create([ 'title' => 'Planning', 'deck_config' => ['name' => 'Fibonacci', 'cards' => []], ]); $previouslyInactive = $room->players()->create([ 'name' => 'Inactive', 'last_seen_at' => now(), ]); $absent = $room->players()->create([ 'name' => 'Nico', 'last_seen_at' => now()->subHours(2), ]); $present = $room->players()->create([ 'name' => 'Anna', 'last_seen_at' => now(), ]); DB::table('players')->where('id', $previouslyInactive->id)->update(['inactive_at' => now()]); $migration = require database_path('migrations/2026_09_24_123048_remove_previously_inactive_players.php'); $migration->up(); $this->assertModelMissing($previouslyInactive); $this->assertModelMissing($absent); $this->assertModelExists($present); expect(Schema::hasColumn('players', 'inactive_at'))->toBeFalse(); }); test('the timeout removes absent players and their votes but keeps recent players', function () { $room = Room::create([ 'title' => 'Planning', 'current_round' => 1, 'deck_config' => ['name' => 'Fibonacci', 'cards' => []], ]); $absentPlayer = $room->players()->create([ 'name' => 'Nico', 'last_seen_at' => now()->subHour(), ]); $recentPlayer = $room->players()->create([ 'name' => 'Anna', 'last_seen_at' => now()->subHour()->addSecond(), ]); $vote = $absentPlayer->votes()->create([ 'room_id' => $room->id, 'round' => $room->current_round, 'value' => '3', ]); $previousVersion = $room->fresh()->state_version; Event::fake([RoomStateChanged::class]); app()->call([new RemoveAbsentPlayers, 'handle']); app()->call([new RemoveAbsentPlayers, 'handle']); $this->assertModelMissing($absentPlayer); $this->assertModelMissing($vote); $this->assertModelExists($recentPlayer); expect($room->fresh()->state_version)->toBe($previousVersion + 1); Event::assertDispatched( RoomStateChanged::class, fn (RoomStateChanged $event): bool => $event->change === RoomStateChange::PlayerKicked && $event->affectedPlayerId === $absentPlayer->id && collect($event->state['players'])->doesntContain('id', $absentPlayer->id), ); Event::assertDispatchedTimes(RoomStateChanged::class, 1); $this->post(route('room.code'), [ 'join_code' => $room->join_code, 'player_name' => 'Nico', ])->assertRedirect(route('room.show', $room)); expect($room->players()->where('name', 'Nico')->sole()->id)->not->toBe($absentPlayer->id); });