set('vision.vector_gateway.enabled', true); config()->set('vision.vector_gateway.base_url', 'https://vision.klevze.net'); config()->set('vision.vector_gateway.api_key', 'test-key'); config()->set('vision.vector_gateway.upsert_endpoint', '/vectors/upsert'); config()->set('vision.vector_gateway.search_endpoint', '/vectors/search'); config()->set('vision.image_variant', 'md'); config()->set('cdn.files_url', 'https://files.skinbase.org'); }); it('indexes artworks into the vector gateway with artwork metadata', function (): void { $contentType = ContentType::query()->create([ 'name' => 'Photography', 'slug' => 'photography', 'description' => '', ]); $category = Category::query()->create([ 'content_type_id' => $contentType->id, 'parent_id' => null, 'name' => 'Abstract', 'slug' => 'abstract', 'description' => '', 'is_active' => true, 'sort_order' => 0, ]); $artwork = Artwork::factory()->create([ 'hash' => 'aabbcc112233', 'thumb_ext' => 'webp', ]); $artwork->categories()->attach($category->id); $tag = Tag::query()->create(['name' => 'Skyline', 'slug' => 'skyline']); $artwork->tags()->attach($tag->id, ['source' => 'ai', 'confidence' => 0.88]); Http::fake([ 'https://vision.klevze.net/vectors/upsert' => Http::response(['ok' => true], 200), ]); artisan('artworks:vectors-index', ['--limit' => 1]) ->assertSuccessful(); $artwork->refresh(); expect($artwork->last_vector_indexed_at)->not->toBeNull(); Http::assertSent(function ($request) use ($artwork): bool { if ($request->url() !== 'https://vision.klevze.net/vectors/upsert') { return false; } $payload = json_decode($request->body(), true); return $request->hasHeader('X-API-Key', 'test-key') && is_array($payload) && ($payload['id'] ?? null) === (string) $artwork->id && ($payload['url'] ?? null) === 'https://files.skinbase.org/md/aa/bb/aabbcc112233.webp' && ($payload['metadata']['content_type'] ?? null) === 'Photography' && ($payload['metadata']['category'] ?? null) === 'Abstract' && ($payload['metadata']['tags'] ?? null) === ['skyline']; }); }); it('searches similar artworks through the vector gateway', function (): void { $contentType = ContentType::query()->create([ 'name' => 'Wallpapers', 'slug' => 'wallpapers', 'description' => '', ]); $category = Category::query()->create([ 'content_type_id' => $contentType->id, 'parent_id' => null, 'name' => 'Nature', 'slug' => 'nature', 'description' => '', 'is_active' => true, 'sort_order' => 0, ]); $source = Artwork::factory()->create([ 'title' => 'Source artwork', 'hash' => 'aabbcc112233', 'thumb_ext' => 'webp', ]); $source->categories()->attach($category->id); $similar = Artwork::factory()->create([ 'title' => 'Nearby artwork', 'hash' => 'ddeeff445566', 'thumb_ext' => 'webp', ]); $similar->categories()->attach($category->id); Http::fake([ 'https://vision.klevze.net/vectors/search' => Http::response([ 'results' => [ ['id' => $source->id, 'score' => 1.0], ['id' => $similar->id, 'score' => 0.9876], ], ], 200), ]); artisan('artworks:vectors-search', [ 'artwork_id' => $source->id, '--limit' => 5, ]) ->expectsTable(['ID', 'Score', 'Title', 'Content Type', 'Category'], [[ 'id' => $similar->id, 'score' => '0.9876', 'title' => 'Nearby artwork', 'content_type' => 'Wallpapers', 'category' => 'Nature', ]]) ->assertSuccessful(); }); it('can re-upsert only artworks that already have local embeddings', function (): void { $contentType = ContentType::query()->create([ 'name' => 'Photography', 'slug' => 'photography', 'description' => '', ]); $category = Category::query()->create([ 'content_type_id' => $contentType->id, 'parent_id' => null, 'name' => 'Portraits', 'slug' => 'portraits', 'description' => '', 'is_active' => true, 'sort_order' => 0, ]); $embeddedArtwork = Artwork::factory()->create([ 'hash' => '112233445566', 'thumb_ext' => 'webp', 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(), ]); $embeddedArtwork->categories()->attach($category->id); ArtworkEmbedding::query()->create([ 'artwork_id' => $embeddedArtwork->id, 'model' => 'clip', 'model_version' => 'v1', 'algo_version' => 'clip-cosine-v1', 'dim' => 2, 'embedding_json' => json_encode([0.6, 0.8], JSON_THROW_ON_ERROR), 'source_hash' => '112233445566', 'is_normalized' => true, 'generated_at' => now(), 'meta' => ['source' => 'clip'], ]); $nonEmbeddedArtwork = Artwork::factory()->create([ 'hash' => 'aabbccddeeff', 'thumb_ext' => 'webp', 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subDay(), ]); $nonEmbeddedArtwork->categories()->attach($category->id); Http::fake([ 'https://vision.klevze.net/vectors/upsert' => Http::response(['ok' => true], 200), ]); artisan('artworks:vectors-index', ['--embedded-only' => true, '--limit' => 10]) ->assertSuccessful(); $embeddedArtwork->refresh(); $nonEmbeddedArtwork->refresh(); expect($embeddedArtwork->last_vector_indexed_at)->not->toBeNull() ->and($nonEmbeddedArtwork->last_vector_indexed_at)->toBeNull(); Http::assertSentCount(1); Http::assertSent(function ($request) use ($embeddedArtwork): bool { if ($request->url() !== 'https://vision.klevze.net/vectors/upsert') { return false; } $payload = json_decode($request->body(), true); return is_array($payload) && ($payload['id'] ?? null) === (string) $embeddedArtwork->id && ($payload['url'] ?? null) === 'https://files.skinbase.org/md/11/22/112233445566.webp'; }); });