124 lines
3.5 KiB
PHP
124 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\IngestUserDiscoveryEventJob;
|
|
use App\Models\Artwork;
|
|
use App\Models\Category;
|
|
use App\Models\ContentType;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('queues discovery event ingestion asynchronously', function () {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->postJson('/api/discovery/events', [
|
|
'event_type' => 'view',
|
|
'artwork_id' => $artwork->id,
|
|
'meta' => ['source' => 'artwork_show'],
|
|
]);
|
|
|
|
$response
|
|
->assertStatus(202)
|
|
->assertJsonPath('queued', true)
|
|
->assertJsonPath('algo_version', (string) config('discovery.algo_version'));
|
|
|
|
Queue::assertPushed(IngestUserDiscoveryEventJob::class, function (IngestUserDiscoveryEventJob $job) use ($user, $artwork): bool {
|
|
return $job->userId === $user->id
|
|
&& $job->artworkId === $artwork->id
|
|
&& $job->eventType === 'view';
|
|
});
|
|
});
|
|
|
|
it('validates discovery event payload', function () {
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->postJson('/api/discovery/events', [
|
|
'event_type' => 'impression',
|
|
'artwork_id' => $artwork->id,
|
|
]);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonValidationErrors(['event_type']);
|
|
});
|
|
|
|
it('accepts session-oriented discovery events', function () {
|
|
Queue::fake();
|
|
|
|
$user = User::factory()->create();
|
|
$artwork = Artwork::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->postJson('/api/discovery/events', [
|
|
'event_type' => 'dwell',
|
|
'artwork_id' => $artwork->id,
|
|
'meta' => ['duration_ms' => 4500],
|
|
]);
|
|
|
|
$response->assertStatus(202);
|
|
|
|
Queue::assertPushed(IngestUserDiscoveryEventJob::class, function (IngestUserDiscoveryEventJob $job): bool {
|
|
return $job->eventType === 'dwell';
|
|
});
|
|
});
|
|
|
|
it('stores discovery event meta as json when ingesting queued events', function (): void {
|
|
$user = User::factory()->create();
|
|
|
|
$contentType = ContentType::query()->create([
|
|
'name' => 'Skins',
|
|
'slug' => 'skins',
|
|
]);
|
|
|
|
$category = Category::query()->create([
|
|
'content_type_id' => $contentType->id,
|
|
'name' => 'Audio',
|
|
'slug' => 'audio',
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$artwork = Artwork::factory()->create([
|
|
'user_id' => $user->id,
|
|
]);
|
|
|
|
DB::table('artwork_category')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'category_id' => $category->id,
|
|
]);
|
|
|
|
$meta = [
|
|
'source' => 'feed',
|
|
'context' => [
|
|
'slot' => 'for-you',
|
|
'score' => 0.88,
|
|
],
|
|
];
|
|
|
|
$job = new IngestUserDiscoveryEventJob(
|
|
eventId: (string) \Illuminate\Support\Str::uuid(),
|
|
userId: $user->id,
|
|
artworkId: $artwork->id,
|
|
eventType: 'view',
|
|
algoVersion: 'clip-cosine-v2-adaptive',
|
|
occurredAt: now()->toIso8601String(),
|
|
meta: $meta,
|
|
);
|
|
|
|
$job->handle(
|
|
app(\App\Services\Recommendations\UserInterestProfileService::class),
|
|
app(\App\Services\Recommendations\SessionRecoService::class),
|
|
);
|
|
|
|
$storedMeta = DB::table('user_discovery_events')->value('meta');
|
|
|
|
expect($storedMeta)->not->toBeNull();
|
|
expect(json_decode((string) $storedMeta, true))->toBe($meta);
|
|
});
|