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