create([ 'name' => 'Related '.$suffix, 'slug' => $suffix.'-type', 'description' => '', ]); return Category::query()->create([ 'content_type_id' => $type->id, 'name' => 'Related '.$suffix, 'slug' => $suffix, 'description' => '', 'is_active' => true, 'sort_order' => 1, ]); } function relatedArtwork(User $user, string $slug, int $minutes): Artwork { return Artwork::factory()->for($user)->create([ 'slug' => $slug, 'title' => $slug, 'published_at' => now()->subMinutes($minutes), ]); } it('uses the split path for rare categories, deduplicates, orders, excludes source, and loads page relations', function (): void { $author = User::factory()->create(); $category = relatedCategory('rare'); $source = relatedArtwork($author, 'source', 100); $source->categories()->attach($category); $sameAuthor = relatedArtwork($author, 'same-author', 5); $sameAuthor->categories()->attach($category); $sameCategory = relatedArtwork(User::factory()->create(), 'same-category', 2); $sameCategory->categories()->attach($category); $queries = []; DB::listen(function ($query) use (&$queries): void { $queries[] = $query->sql; }); $results = app(ArtworkRelatedService::class)->related($source->fresh(['categories', 'tags']), null); expect($results->pluck('id')->all())->toBe([$sameCategory->id, $sameAuthor->id]) ->and($results->first()->relationLoaded('user'))->toBeTrue() ->and($results->first()->relationLoaded('group'))->toBeTrue() ->and($results->first()->relationLoaded('categories'))->toBeTrue() ->and($results->first()->categories->first()->relationLoaded('contentType'))->toBeTrue() ->and(collect($queries)->contains(fn (string $sql): bool => str_contains(strtolower($sql), 'join') && str_contains(strtolower($sql), 'artwork_category')))->toBeTrue() ->and($results->pluck('id'))->not->toContain($source->id); }); it('keeps the original OR/EXISTS path when there are no categories', function (): void { $author = User::factory()->create(); $source = relatedArtwork($author, 'source-without-category', 100); $related = relatedArtwork($author, 'author-without-category', 1); DB::enableQueryLog(); $results = app(ArtworkRelatedService::class)->related($source->fresh(['categories', 'tags']), null); $sql = collect(DB::getQueryLog())->pluck('query')->implode("\n"); expect($results->pluck('id')->all())->toContain($related->id) ->and(strtolower($sql))->toContain('select'); }); it('applies the same maturity filter to split candidates for guests and viewers', function (): void { config()->set('maturity.viewer.guest_mode', ArtworkMaturityService::VIEW_HIDE); config()->set('maturity.viewer.default_mode', ArtworkMaturityService::VIEW_HIDE); $author = User::factory()->create(); $category = relatedCategory('maturity'); $source = relatedArtwork($author, 'maturity-source', 100); $source->categories()->attach($category); $safe = relatedArtwork(User::factory()->create(), 'safe-related', 2); $safe->categories()->attach($category); $mature = relatedArtwork(User::factory()->create(), 'mature-related', 1); $mature->forceFill(['is_mature' => true])->save(); $mature->categories()->attach($category); $guestIds = app(ArtworkRelatedService::class)->related($source->fresh(['categories', 'tags']), null)->pluck('id'); expect($guestIds)->toContain($safe->id)->not->toContain($mature->id); }); it('retains the current query above the 1000 matching pivot-row threshold', function (): void { $author = User::factory()->create(); $category = relatedCategory('threshold'); $source = relatedArtwork($author, 'threshold-source', 2000); $source->categories()->attach($category); $rows = Artwork::factory()->count(1000)->create()->each(function (Artwork $artwork) use ($category): void { $artwork->categories()->attach($category); }); DB::enableQueryLog(); app(ArtworkRelatedService::class)->related($source->fresh(['categories', 'tags']), null); $sql = collect(DB::getQueryLog())->pluck('query')->implode("\n"); expect($rows)->toHaveCount(1000) ->and(strtolower($sql))->toContain('exists (select'); });