create(); $artworkA = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subMinutes(2)]); $artworkB = Artwork::factory()->create(['is_public' => true, 'is_approved' => true, 'published_at' => now()->subMinutes(1)]); DB::table('artwork_stats')->insert([ ['artwork_id' => $artworkA->id, 'views' => 120, 'downloads' => 30, 'favorites' => 2, 'rating_avg' => 0, 'rating_count' => 0], ['artwork_id' => $artworkB->id, 'views' => 100, 'downloads' => 20, 'favorites' => 1, 'rating_avg' => 0, 'rating_count' => 0], ]); app(PersonalizedFeedService::class)->regenerateCacheForUser($user->id, (string) config('discovery.algo_version')); $cache = UserRecommendationCache::query() ->where('user_id', $user->id) ->where('algo_version', (string) config('discovery.algo_version')) ->first(); expect($cache)->not->toBeNull(); expect($cache?->generated_at)->not->toBeNull(); expect($cache?->expires_at)->not->toBeNull(); $items = (array) ($cache?->recommendations_json['items'] ?? []); expect(count($items))->toBeGreaterThan(0); expect((int) ($items[0]['artwork_id'] ?? 0))->toBeGreaterThan(0); }); it('uses rollout gate g100 to select candidate algo version', function () { $user = User::factory()->create(); config()->set('discovery.rollout.enabled', true); config()->set('discovery.rollout.baseline_algo_version', 'clip-cosine-v1'); config()->set('discovery.rollout.candidate_algo_version', 'clip-cosine-v2'); config()->set('discovery.rollout.active_gate', 'g100'); config()->set('discovery.rollout.gates.g100.percentage', 100); config()->set('discovery.rollout.force_algo_version', ''); app(PersonalizedFeedService::class)->regenerateCacheForUser($user->id); $cache = UserRecommendationCache::query()->where('user_id', $user->id)->first(); expect($cache)->not->toBeNull(); expect((string) $cache?->algo_version)->toBe('clip-cosine-v2'); }); it('forces rollback algo version when force toggle is set', function () { $user = User::factory()->create(); config()->set('discovery.rollout.enabled', true); config()->set('discovery.rollout.baseline_algo_version', 'clip-cosine-v1'); config()->set('discovery.rollout.candidate_algo_version', 'clip-cosine-v2'); config()->set('discovery.rollout.active_gate', 'g100'); config()->set('discovery.rollout.gates.g100.percentage', 100); config()->set('discovery.rollout.force_algo_version', 'clip-cosine-v1'); app(PersonalizedFeedService::class)->regenerateCacheForUser($user->id); $cache = UserRecommendationCache::query()->where('user_id', $user->id)->first(); expect($cache)->not->toBeNull(); expect((string) $cache?->algo_version)->toBe('clip-cosine-v1'); });