Ship production optimization M1-M12.5A: queues, metrics, HTTP observability, and vector search reliability.
Keep similar-ai from tripping the global circuit on a lone URL 502, clamp Qdrant search to 100, and add Server-Timing plus slow-request logging. Studio shared props, Academy S3 exists caching, heat chunking, and Redis/scheduler hygiene stay in this rollout.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Recommendations;
|
||||
|
||||
use App\Jobs\RecComputeSimilarByBehaviorJob;
|
||||
use App\Jobs\RecComputeSimilarByTagsJob;
|
||||
use App\Jobs\RecComputeSimilarHybridJob;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\RecArtworkRec;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class RecComputeSimilarJobsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_redis_retry_after_exceeds_horizon_default_timeout(): void
|
||||
{
|
||||
$this->assertGreaterThan(
|
||||
(int) config('horizon.defaults.supervisor-default.timeout'),
|
||||
(int) config('queue.connections.redis.retry_after'),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_behavior_job_processes_a_batch_and_dispatches_the_next_cursor(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$artworks = Artwork::factory()->count(3)->create(['user_id' => $user->id]);
|
||||
|
||||
DB::table('rec_item_pairs')->insert([
|
||||
[
|
||||
'a_artwork_id' => $artworks[0]->id,
|
||||
'b_artwork_id' => $artworks[1]->id,
|
||||
'weight' => 1.5,
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'a_artwork_id' => $artworks[0]->id,
|
||||
'b_artwork_id' => $artworks[2]->id,
|
||||
'weight' => 1.0,
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
Queue::fake();
|
||||
|
||||
(new RecComputeSimilarByBehaviorJob(null, 2))->handle();
|
||||
|
||||
$this->assertNotNull(
|
||||
RecArtworkRec::query()
|
||||
->where('artwork_id', $artworks[0]->id)
|
||||
->where('rec_type', 'similar_behavior')
|
||||
->first(),
|
||||
);
|
||||
|
||||
$expectedCursor = (int) $artworks->sortBy('id')->values()[1]->id;
|
||||
|
||||
Queue::assertPushed(
|
||||
RecComputeSimilarByBehaviorJob::class,
|
||||
fn (RecComputeSimilarByBehaviorJob $job): bool => $job->cursorAfterArtworkId() === $expectedCursor,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_behavior_job_does_not_dispatch_when_the_batch_is_partial(): void
|
||||
{
|
||||
Artwork::factory()->count(2)->create();
|
||||
|
||||
Queue::fake();
|
||||
|
||||
(new RecComputeSimilarByBehaviorJob(null, 10))->handle();
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
}
|
||||
|
||||
public function test_hybrid_job_is_idempotent_for_a_single_artwork(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$source = Artwork::factory()->create(['user_id' => $user->id]);
|
||||
$related = Artwork::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
RecArtworkRec::query()->create([
|
||||
'artwork_id' => $source->id,
|
||||
'rec_type' => 'similar_tags',
|
||||
'model_version' => (string) config('recommendations.similarity.model_version', 'sim_v1'),
|
||||
'recs' => [$related->id],
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
|
||||
(new RecComputeSimilarHybridJob($source->id))->handle();
|
||||
(new RecComputeSimilarHybridJob($source->id))->handle();
|
||||
|
||||
$this->assertSame(
|
||||
1,
|
||||
RecArtworkRec::query()
|
||||
->where('artwork_id', $source->id)
|
||||
->where('rec_type', 'similar_hybrid')
|
||||
->count(),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_hybrid_batch_dispatches_continuation_when_full(): void
|
||||
{
|
||||
Artwork::factory()->count(3)->create();
|
||||
|
||||
Queue::fake();
|
||||
|
||||
(new RecComputeSimilarHybridJob(null, 2))->handle();
|
||||
|
||||
Queue::assertPushed(RecComputeSimilarHybridJob::class);
|
||||
}
|
||||
|
||||
public function test_tag_idf_group_by_is_cached_across_handle_calls(): void
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
Artwork::factory()->create([
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now(),
|
||||
]);
|
||||
|
||||
$idfQueries = 0;
|
||||
DB::listen(function ($query) use (&$idfQueries): void {
|
||||
if (str_contains(strtolower($query->sql), 'artwork_tag')
|
||||
&& str_contains(strtolower($query->sql), 'group by')) {
|
||||
$idfQueries++;
|
||||
}
|
||||
});
|
||||
|
||||
(new RecComputeSimilarByTagsJob(null, 10))->handle();
|
||||
(new RecComputeSimilarByTagsJob(null, 10))->handle();
|
||||
|
||||
$this->assertSame(1, $idfQueries);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user