create(['role' => 'admin']); $artworkA = Artwork::factory()->create(['title' => 'Feed Artwork A']); $artworkB = Artwork::factory()->create(['title' => 'Feed Artwork B']); $metricDate = now()->subDay()->toDateString(); DB::table('feed_daily_metrics')->insert([ [ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'impressions' => 10, 'clicks' => 4, 'saves' => 2, 'ctr' => 0.4, 'save_rate' => 0.5, 'dwell_0_5' => 1, 'dwell_5_30' => 1, 'dwell_30_120' => 1, 'dwell_120_plus' => 1, 'created_at' => now(), 'updated_at' => now(), ], ]); DB::table('feed_events')->insert([ [ 'event_date' => $metricDate, 'event_type' => 'feed_impression', 'user_id' => $admin->id, 'artwork_id' => $artworkA->id, 'position' => 1, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => null, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], [ 'event_date' => $metricDate, 'event_type' => 'feed_click', 'user_id' => $admin->id, 'artwork_id' => $artworkA->id, 'position' => 1, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 12, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], [ 'event_date' => $metricDate, 'event_type' => 'feed_click', 'user_id' => $admin->id, 'artwork_id' => $artworkB->id, 'position' => 2, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'dwell_seconds' => 7, 'occurred_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], ]); $response = $this->actingAs($admin)->getJson('/api/admin/reports/feed-performance?from=' . $metricDate . '&to=' . $metricDate); $response->assertOk(); $response->assertJsonPath('meta.from', $metricDate); $response->assertJsonPath('meta.to', $metricDate); $rows = collect($response->json('by_algo_source')); expect($rows->count())->toBe(1); expect($rows->first()['algo_version'])->toBe('clip-cosine-v1'); expect($rows->first()['source'])->toBe('personalized'); expect((float) $rows->first()['ctr'])->toBe(0.4); $top = collect($response->json('top_clicked_artworks')); expect($top->isNotEmpty())->toBeTrue(); expect((int) $top->first()['artwork_id'])->toBe($artworkA->id); }); it('non-admin is denied feed performance report endpoint', function () { $user = User::factory()->create(['role' => 'user']); $response = $this->actingAs($user)->getJson('/api/admin/reports/feed-performance'); $response->assertStatus(403); });