init project

This commit is contained in:
2026-05-10 13:21:24 +03:30
parent 5f210c6c73
commit dc36701c9d
26 changed files with 2149 additions and 69 deletions

View File

@@ -1,5 +1,6 @@
<?php
use App\Models\EmbeddingUsageEvent;
use App\Models\User;
test('guests are redirected to the login page', function () {
@@ -12,5 +13,49 @@ test('authenticated users can visit the dashboard', function () {
$this->actingAs($user);
$response = $this->get(route('dashboard'));
$response->assertOk();
});
$response->assertOk()
->assertSee(__('Usage overview'))
->assertSee(__('Tool mix'))
->assertSee(__('Recent activity'));
});
test('dashboard shows usage charts for the authenticated user', function () {
$user = User::factory()->create();
$otherUser = User::factory()->create();
EmbeddingUsageEvent::factory()->for($user)->create([
'tool' => 'embed',
'model' => 'nomic-embed-text',
'input_count' => 3,
'result_count' => 3,
'created_at' => now(),
]);
EmbeddingUsageEvent::factory()->for($user)->create([
'tool' => 'search',
'model' => 'nomic-embed-text',
'input_count' => 1,
'result_count' => 2,
'created_at' => now()->subDay(),
]);
EmbeddingUsageEvent::factory()->for($otherUser)->create([
'tool' => 'compare',
'model' => 'other-user-model',
'input_count' => 9,
'result_count' => 36,
'created_at' => now(),
]);
$this->actingAs($user);
$this->get(route('dashboard'))
->assertOk()
->assertSee(__('Actions'))
->assertSee(__('Phrases'))
->assertSee(__('Models'))
->assertSee('nomic-embed-text')
->assertSee(__('Embed'))
->assertSee(__('Search'))
->assertDontSee('other-user-model');
});

View 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,
]);
});