Files
SkinbaseNova/tests/Feature/Recommendations/RecComputeSimilarJobsTest.php
T
klevze 8a80aae21e 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.
2026-08-25 07:58:47 +02:00

142 lines
4.3 KiB
PHP

<?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);
}
}