init project
This commit is contained in:
235
tests/Feature/EmbeddingToolsTest.php
Normal file
235
tests/Feature/EmbeddingToolsTest.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
use App\Models\EmbeddingEntry;
|
||||
use App\Models\EmbeddingUsageEvent;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Laravel\Ai\Embeddings;
|
||||
use Laravel\Ai\Prompts\EmbeddingsPrompt;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'ai.default_for_embeddings' => '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,
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user