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.search_point_endpoint', '/vectors/search/point'); config()->set('cdn.files_url', 'https://files.skinbase.org'); config()->set('app.url', 'https://skinbase.test'); Cache::flush(); Storage::fake('public'); }); function pointSearchArtwork(): Artwork { return Artwork::factory()->create([ 'hash' => 'point-source-hash', 'thumb_ext' => 'webp', 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subHour(), ]); } it('uses the stored point and does not fetch or embed the source image', function (): void { $source = pointSearchArtwork(); $match = Artwork::factory()->create(['title' => 'Point match']); Http::fake([ 'https://vision.klevze.net/vectors/search/point' => Http::response([ 'results' => [ ['id' => $source->id, 'score' => 1.0], ['id' => $match->id, 'score' => 0.91], ], ], 200), 'https://files.skinbase.org/*' => Http::response('should not download', 500), 'https://vision.klevze.net/vectors/search/file' => Http::response(['error' => 'should not embed'], 500), 'https://vision.klevze.net/vectors/search' => Http::response(['error' => 'should not search by URL'], 500), ]); getJson('/api/art/' . $source->id . '/similar-ai') ->assertOk() ->assertJsonPath('data.0.id', $match->id); Http::assertSent(fn ($request): bool => str_contains($request->url(), '/vectors/search/point') && ($request->data()['id'] ?? null) === $source->id); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), '/vectors/search/file')); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), '/vectors/search') && ! str_contains($request->url(), '/point')); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'files.skinbase.org')); }); it('falls back to the existing image path only when the point is missing', function (): void { $source = pointSearchArtwork(); $match = Artwork::factory()->create(['title' => 'Fallback match']); Http::fake([ 'https://vision.klevze.net/vectors/search/point' => Http::response(['detail' => 'missing'], 404), 'https://files.skinbase.org/*' => Http::response('image-bytes', 200, ['Content-Type' => 'image/webp']), 'https://vision.klevze.net/vectors/search/file' => Http::response([ 'results' => [['id' => $match->id, 'score' => 0.8]], ], 200), ]); getJson('/api/art/' . $source->id . '/similar-ai') ->assertOk() ->assertJsonPath('data.0.id', $match->id); }); it('does not fall back to CLIP after a point-search server failure', function (): void { $source = pointSearchArtwork(); Http::fake([ 'https://vision.klevze.net/vectors/search/point' => Http::response(['error' => 'upstream'], 500), 'https://files.skinbase.org/*' => Http::response('should not download', 500), 'https://vision.klevze.net/vectors/search/file' => Http::response(['error' => 'should not embed'], 500), 'https://vision.klevze.net/vectors/search' => Http::response(['error' => 'should not search by URL'], 500), ]); getJson('/api/art/' . $source->id . '/similar-ai')->assertStatus(502); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), 'files.skinbase.org')); Http::assertNotSent(fn ($request): bool => str_contains($request->url(), '/vectors/search/file')); }); it('does not fall back to CLIP after a point-search timeout', function (): void { $source = pointSearchArtwork(); Http::fake(function ($request) { if (str_contains($request->url(), '/vectors/search/point')) { throw new ConnectionException('cURL error 28: Operation timed out'); } throw new RuntimeException('fallback must not be called'); }); getJson('/api/art/' . $source->id . '/similar-ai')->assertStatus(502); Http::assertNothingSent(); });