'ollama', 'ai.providers.ollama.url' => 'http://ollama.test', 'ai.providers.ollama.models.embeddings.default' => 'test-embed', 'ai.providers.ollama.models.embeddings.available' => ['test-embed'], 'ai.providers.ollama.models.embeddings.dimensions' => 3, ]); Http::preventStrayRequests(); Http::fake([ 'http://ollama.test/api/tags' => Http::response([ 'models' => [ ['name' => 'test-embed'], ['name' => 'alternate-embed'], ], ]), ]); }); test('guests are redirected from embedding pages', function (string $route) { $this->get(route($route))->assertRedirect(route('login')); })->with([ 'redirect' => 'embeddings.tools', 'embed' => 'embeddings.embed', 'search' => 'embeddings.search', 'compare' => 'embeddings.compare', ]); test('embeddings index redirects to embed page', function () { $this->actingAs(User::factory()->create()); $this->get(route('embeddings.tools'))->assertRedirect(route('embeddings.embed')); }); test('authenticated users can open embedding pages', function (string $route, string $text) { $this->actingAs(User::factory()->create()); $this->get(route($route)) ->assertOk() ->assertSee($text); })->with([ 'embed' => ['embeddings.embed', 'Embed'], 'search' => ['embeddings.search', 'Similarity search'], 'compare' => ['embeddings.compare', 'Proximity comparison'], ]); test('comparison page renders separate phrase inputs', function () { $this->actingAs(User::factory()->create()); $this->get(route('embeddings.compare')) ->assertOk() ->assertSee(__('Phrase 1')) ->assertSee(__('Phrase 2')) ->assertSee(__('Phrase 3')) ->assertSee(__('Add phrase')); }); test('comparison phrase inputs can be added and removed', function () { $this->actingAs(User::factory()->create()); Livewire::test('pages::embeddings.compare') ->assertSet('comparisonPhrases', ['', '', '']) ->call('addComparisonPhrase') ->assertSet('comparisonPhrases', ['', '', '', '']) ->call('removeComparisonPhrase', 1) ->assertSet('comparisonPhrases', ['', '', '']); }); test('app header has a menu item for each embedding tool', function () { $this->actingAs(User::factory()->create()); $this->get(route('dashboard')) ->assertOk() ->assertSee(__('Embed')) ->assertSee(__('Search')) ->assertSee(__('Compare')) ->assertDontSee(__('Repository')) ->assertDontSee(__('Documentation')); }); test('embedding pages do not duplicate tool navigation below the header', function () { $this->actingAs(User::factory()->create()); $response = $this->get(route('embeddings.embed'))->assertOk(); expect(substr_count($response->getContent(), route('embeddings.embed')))->toBe(2) ->and(substr_count($response->getContent(), route('embeddings.search')))->toBe(2) ->and(substr_count($response->getContent(), route('embeddings.compare')))->toBe(2); }); test('embedding model options are fetched from ollama', function () { $this->actingAs(User::factory()->create()); $component = Livewire::test('pages::embeddings.embed'); $modelNames = collect($component->get('modelOptions'))->pluck('name')->all(); expect($modelNames)->toBe(['test-embed', 'alternate-embed']); Http::assertSent(fn ($request): bool => $request->url() === 'http://ollama.test/api/tags'); }); test('first ollama model is selected when configured default is unavailable', function () { config(['ai.providers.ollama.models.embeddings.default' => 'missing-default']); $this->actingAs(User::factory()->create()); Livewire::test('pages::embeddings.embed') ->assertSet('embeddingModel', 'test-embed'); }); test('phrases can be embedded and stored', function () { Embeddings::fake([ [ [1, 0, 0], [0, 1, 0], ], ]); $this->actingAs(User::factory()->create()); $component = Livewire::test('pages::embeddings.embed') ->set('embeddingInput', "apple\nbanana") ->set('embeddingModel', 'alternate-embed') ->call('generateEmbeddings') ->assertHasNoErrors(); $usageEvent = EmbeddingUsageEvent::query()->first(); expect(EmbeddingEntry::query()->count())->toBe(2) ->and(EmbeddingUsageEvent::query()->count())->toBe(1) ->and($usageEvent?->only(['user_id', 'tool', 'model', 'input_count', 'result_count']))->toMatchArray([ 'user_id' => auth()->id(), 'tool' => 'embed', 'model' => 'alternate-embed', 'input_count' => 2, 'result_count' => 2, ]) ->and($component->get('storedCount'))->toBe(2) ->and($component->get('embeddingResults'))->toHaveCount(2); Embeddings::assertGenerated(fn (EmbeddingsPrompt $prompt): bool => $prompt->inputs === ['apple', 'banana'] && $prompt->dimensions === 3 && $prompt->model === 'alternate-embed'); }); test('similarity search ranks stored embeddings', function () { Embeddings::fake([ [ [1, 0, 0], [0.8, 0.2, 0], [0, 1, 0], ], [ [1, 0, 0], ], ]); $this->actingAs(User::factory()->create()); Livewire::test('pages::embeddings.embed') ->set('embeddingInput', "alpha\nnear alpha\nbeta") ->set('embeddingModel', 'alternate-embed') ->call('generateEmbeddings') ->assertHasNoErrors(); $component = Livewire::test('pages::embeddings.search') ->set('similarityQuery', 'alpha query') ->set('embeddingModel', 'alternate-embed') ->set('minimumSimilarity', '0.70') ->set('similarityLimit', 5) ->call('searchSimilar') ->assertHasNoErrors(); $results = $component->get('similarityResults'); $usageEvent = EmbeddingUsageEvent::query()->where('tool', 'search')->first(); expect($results)->toHaveCount(2) ->and(array_column($results, 'source_text'))->toBe(['alpha', 'near alpha']) ->and($results[0]['similarity'])->toBe(1.0) ->and($usageEvent?->only(['model', 'input_count', 'result_count']))->toMatchArray([ 'model' => 'alternate-embed', 'input_count' => 1, 'result_count' => 2, ]); }); test('comparison builds a pairwise similarity matrix', function () { Embeddings::fake([ [ [1, 0, 0], [0, 1, 0], [0.7, 0.7, 0], ], ]); $this->actingAs(User::factory()->create()); $component = Livewire::test('pages::embeddings.compare') ->set('comparisonPhrases', ['red', 'blue', 'purple']) ->set('embeddingModel', 'alternate-embed') ->call('comparePhrases') ->assertHasNoErrors(); $matrix = $component->get('comparisonMatrix'); $pairs = $component->get('comparisonPairs'); $closestPair = $component->get('closestPair'); $usageEvent = EmbeddingUsageEvent::query()->where('tool', 'compare')->first(); expect($matrix)->toHaveCount(3) ->and($matrix[0]['scores'][0])->toBe(1.0) ->and($matrix[0]['scores'][1])->toBe(0.0) ->and($matrix[0]['scores'][2])->toBe(0.707107) ->and($pairs)->toHaveCount(3) ->and(collect($pairs)->contains(fn (array $pair): bool => $pair['first'] === 'red' && $pair['second'] === 'purple' && $pair['similarity'] === 0.707107))->toBeTrue() ->and($closestPair['first'])->toBe('red') ->and($closestPair['second'])->toBe('purple') ->and($usageEvent?->only(['model', 'input_count', 'result_count']))->toMatchArray([ 'model' => 'alternate-embed', 'input_count' => 3, 'result_count' => 3, ]); });