subDay()->toDateString(); DB::table('feed_daily_metrics')->insert([ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'impressions' => 100, 'clicks' => 20, 'saves' => 8, 'ctr' => 0.2, 'save_rate' => 0.4, 'dwell_0_5' => 3, 'dwell_5_30' => 7, 'dwell_30_120' => 6, 'dwell_120_plus' => 4, 'created_at' => now(), 'updated_at' => now(), ]); $result = app(FeedOfflineEvaluationService::class)->evaluateAlgo('clip-cosine-v1', $metricDate, $metricDate); expect((string) $result['algo_version'])->toBe('clip-cosine-v1'); expect((float) $result['ctr'])->toBe(0.2); expect((float) $result['save_rate'])->toBe(0.4); expect((float) $result['long_dwell_share'])->toBe(0.5); expect((float) $result['bounce_rate'])->toBe(0.15); expect((float) $result['objective_score'])->toBeGreaterThan(0); }); it('compares baseline vs candidate with delta and lift', function () { $metricDate = now()->subDay()->toDateString(); DB::table('feed_daily_metrics')->insert([ [ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'impressions' => 100, 'clicks' => 20, 'saves' => 6, 'ctr' => 0.2, 'save_rate' => 0.3, 'dwell_0_5' => 4, 'dwell_5_30' => 8, 'dwell_30_120' => 5, 'dwell_120_plus' => 3, 'created_at' => now(), 'updated_at' => now(), ], [ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v2', 'source' => 'personalized', 'impressions' => 100, 'clicks' => 25, 'saves' => 10, 'ctr' => 0.25, 'save_rate' => 0.4, 'dwell_0_5' => 3, 'dwell_5_30' => 8, 'dwell_30_120' => 8, 'dwell_120_plus' => 6, 'created_at' => now(), 'updated_at' => now(), ], ]); $comparison = app(FeedOfflineEvaluationService::class) ->compareBaselineCandidate('clip-cosine-v1', 'clip-cosine-v2', $metricDate, $metricDate); expect((float) $comparison['delta']['objective_score'])->toBeGreaterThan(0.0); expect((float) $comparison['delta']['ctr'])->toBeGreaterThan(0.0); expect((float) $comparison['delta']['save_rate'])->toBeGreaterThan(0.0); }); it('treats save_rate as informational when configured', function () { $metricDate = now()->subDay()->toDateString(); config()->set('discovery.evaluation.objective_weights', [ 'ctr' => 0.45, 'save_rate' => 0.35, 'long_dwell_share' => 0.25, 'bounce_rate_penalty' => 0.15, ]); config()->set('discovery.evaluation.save_rate_informational', true); DB::table('feed_daily_metrics')->insert([ 'metric_date' => $metricDate, 'algo_version' => 'clip-cosine-v1', 'source' => 'personalized', 'impressions' => 100, 'clicks' => 20, 'saves' => 8, 'ctr' => 0.2, 'save_rate' => 0.4, 'dwell_0_5' => 3, 'dwell_5_30' => 7, 'dwell_30_120' => 6, 'dwell_120_plus' => 4, 'created_at' => now(), 'updated_at' => now(), ]); $result = app(FeedOfflineEvaluationService::class)->evaluateAlgo('clip-cosine-v1', $metricDate, $metricDate); expect((float) $result['save_rate'])->toBe(0.4); expect((float) $result['objective_score'])->toBe(0.226471); });