Save workspace changes
This commit is contained in:
167
tests/Feature/Console/AuditMissingMigratedUsersCommandTest.php
Normal file
167
tests/Feature/Console/AuditMissingMigratedUsersCommandTest.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
config()->set('database.connections.legacy', config('database.connections.' . config('database.default')));
|
||||
DB::purge('legacy');
|
||||
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
Schema::connection('legacy')->create('legacy_users', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
$table->string('uname')->nullable();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('real_name')->nullable();
|
||||
$table->timestamp('joinDate')->nullable();
|
||||
$table->timestamp('LastVisit')->nullable();
|
||||
$table->unsignedTinyInteger('active')->default(1);
|
||||
$table->unsignedTinyInteger('should_migrate')->default(0);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
Schema::connection('legacy')->dropIfExists('legacy_users');
|
||||
});
|
||||
|
||||
it('passes when every legacy should_migrate user exists in the new users table', function (): void {
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 101,
|
||||
'username' => 'alpha',
|
||||
'email' => 'alpha@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'username' => 'beta',
|
||||
'email' => 'beta@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::connection('legacy')->table('legacy_users')->insert([
|
||||
['user_id' => 101, 'uname' => 'alpha', 'email' => 'alpha@example.test', 'should_migrate' => 1],
|
||||
['user_id' => 102, 'uname' => 'beta', 'email' => 'beta@example.test', 'should_migrate' => 1],
|
||||
['user_id' => 103, 'uname' => 'gamma', 'email' => 'gamma@example.test', 'should_migrate' => 0],
|
||||
]);
|
||||
|
||||
$code = Artisan::call('users:audit-missing-migrated', [
|
||||
'--legacy-users-table' => 'legacy_users',
|
||||
'--chunk' => 1,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(0)
|
||||
->and($output)->toContain('Scanning legacy.legacy_users for should_migrate=1 and checking')
|
||||
->and($output)->toContain('Done. scanned=2 existing=2 missing=0')
|
||||
->and($output)->not->toContain('[missing]');
|
||||
});
|
||||
|
||||
it('fails and outputs legacy users that are missing in the new users table', function (): void {
|
||||
DB::table('users')->insert([
|
||||
'id' => 201,
|
||||
'username' => 'present-user',
|
||||
'email' => 'present@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::connection('legacy')->table('legacy_users')->insert([
|
||||
['user_id' => 201, 'uname' => 'present-user', 'email' => 'present@example.test', 'should_migrate' => 1],
|
||||
['user_id' => 202, 'uname' => 'missing-user', 'email' => 'missing@example.test', 'should_migrate' => 1],
|
||||
['user_id' => 203, 'uname' => null, 'email' => null, 'should_migrate' => 1],
|
||||
]);
|
||||
|
||||
$code = Artisan::call('users:audit-missing-migrated', [
|
||||
'--legacy-users-table' => 'legacy_users',
|
||||
'--chunk' => 2,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
|
||||
expect($code)->toBe(1)
|
||||
->and($output)->toContain('[missing] id=202 uname=@missing-user email=<missing@example.test>')
|
||||
->and($output)->toContain('[missing] id=203 uname=(none) email=(none)')
|
||||
->and($output)->toContain('Done. scanned=3 existing=1 missing=2');
|
||||
});
|
||||
|
||||
it('can write missing users to a transaction wrapped sql file', function (): void {
|
||||
$sqlPath = base_path('test-results/audit-missing-migrated-users.sql');
|
||||
@unlink($sqlPath);
|
||||
|
||||
DB::table('users')->insert([
|
||||
[
|
||||
'id' => 301,
|
||||
'username' => 'present-user',
|
||||
'email' => 'present@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
[
|
||||
'id' => 999,
|
||||
'username' => 'missing-user',
|
||||
'email' => 'missing@example.test',
|
||||
'password' => bcrypt('secret'),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
],
|
||||
]);
|
||||
|
||||
DB::connection('legacy')->table('legacy_users')->insert([
|
||||
[
|
||||
'user_id' => 301,
|
||||
'uname' => 'present-user',
|
||||
'email' => 'present@example.test',
|
||||
'real_name' => 'Present User',
|
||||
'joinDate' => '2024-01-01 10:00:00',
|
||||
'LastVisit' => '2024-01-02 11:00:00',
|
||||
'active' => 1,
|
||||
'should_migrate' => 1,
|
||||
],
|
||||
[
|
||||
'user_id' => 302,
|
||||
'uname' => 'missing-user',
|
||||
'email' => 'missing@example.test',
|
||||
'real_name' => 'Legacy Missing',
|
||||
'joinDate' => '2023-05-01 08:30:00',
|
||||
'LastVisit' => '2023-05-03 09:45:00',
|
||||
'active' => 0,
|
||||
'should_migrate' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$code = Artisan::call('users:audit-missing-migrated', [
|
||||
'--legacy-users-table' => 'legacy_users',
|
||||
'--sql-output' => $sqlPath,
|
||||
]);
|
||||
|
||||
$output = Artisan::output();
|
||||
$sql = file_get_contents($sqlPath);
|
||||
|
||||
expect($code)->toBe(1)
|
||||
->and($output)->toContain('SQL export written to ' . $sqlPath . ' with 1 INSERT statement(s).')
|
||||
->and($sql)->not->toBeFalse()
|
||||
->and($sql)->toContain('START TRANSACTION;')
|
||||
->and($sql)->toContain('INSERT INTO `users`')
|
||||
->and($sql)->toContain("302, 'tmpu302'")
|
||||
->and($sql)->toContain("'missing+1@example.test'")
|
||||
->and($sql)->toContain("'Legacy Missing'")
|
||||
->and($sql)->toContain('COMMIT;');
|
||||
|
||||
@unlink($sqlPath);
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkAiAssist;
|
||||
use App\Models\Category;
|
||||
use App\Models\ContentType;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
it('generates and stores artwork ai suggestions from the artisan command', function (): void {
|
||||
config()->set('vision.enabled', true);
|
||||
config()->set('vision.gateway.base_url', 'https://vision.local');
|
||||
config()->set('vision.lm_studio.base_url', 'https://lmstudio.local');
|
||||
config()->set('vision.lm_studio.model', 'google/gemma-3-4b');
|
||||
config()->set('cdn.files_url', 'https://files.local');
|
||||
|
||||
$photography = ContentType::query()->create([
|
||||
'name' => 'Photography',
|
||||
'slug' => 'photography',
|
||||
]);
|
||||
|
||||
Category::query()->create([
|
||||
'content_type_id' => $photography->id,
|
||||
'name' => 'Flowers',
|
||||
'slug' => 'flowers',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'hash' => 'cmdaa112233',
|
||||
'file_name' => 'rose-closeup.jpg',
|
||||
'title' => 'Rose Study',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://vision.local/analyze/all' => Http::response([
|
||||
'clip' => [
|
||||
['tag' => 'rose', 'confidence' => 0.96],
|
||||
['tag' => 'flower', 'confidence' => 0.91],
|
||||
],
|
||||
'yolo' => [
|
||||
['label' => 'flower', 'confidence' => 0.79],
|
||||
],
|
||||
'blip' => 'a close up photograph of a rose bud with soft natural background',
|
||||
], 200),
|
||||
'https://lmstudio.local/v1/chat/completions' => Http::response([
|
||||
'choices' => [
|
||||
[
|
||||
'message' => [
|
||||
'content' => json_encode([
|
||||
'rose macro',
|
||||
'flower close-up',
|
||||
'soft petals',
|
||||
'natural light',
|
||||
'botanical photography',
|
||||
'pink tones',
|
||||
'shallow depth',
|
||||
'floral detail',
|
||||
'macro photography',
|
||||
'garden bloom',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('artworks:ai-suggest', ['artwork_id' => $artwork->id])
|
||||
->expectsOutputToContain('provider: lm_studio')
|
||||
->expectsOutputToContain('tags: rose-macro, flower-close-up')
|
||||
->expectsOutputToContain('content type: photography | category: flowers')
|
||||
->assertSuccessful();
|
||||
|
||||
$assist = ArtworkAiAssist::query()->where('artwork_id', $artwork->id)->first();
|
||||
|
||||
expect($assist)->not->toBeNull();
|
||||
expect($assist?->status)->toBe(ArtworkAiAssist::STATUS_READY);
|
||||
expect($assist?->tag_suggestions_json)->not->toBeEmpty();
|
||||
expect(collect($assist?->tag_suggestions_json ?? [])->contains(fn (array $row): bool => ($row['tag'] ?? null) === 'rose-macro'))->toBeTrue();
|
||||
expect($assist?->raw_response_json['tag_generation']['raw_content'] ?? null)->toBe('["rose macro","flower close-up","soft petals","natural light","botanical photography","pink tones","shallow depth","floral detail","macro photography","garden bloom"]');
|
||||
expect($assist?->raw_response_json['tag_generation']['image_url'] ?? null)->toBe('https://files.local/artworks/md/cm/da/cmdaa112233.webp');
|
||||
});
|
||||
|
||||
it('supports overriding the provider to together from the artisan command', function (): void {
|
||||
config()->set('vision.enabled', true);
|
||||
config()->set('vision.gateway.base_url', 'https://vision.local');
|
||||
config()->set('vision.together.base_url', 'https://api.together.xyz');
|
||||
config()->set('vision.together.endpoint', '/v1/chat/completions');
|
||||
config()->set('vision.together.model', 'meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo');
|
||||
config()->set('vision.together.api_key', 'together-test-key');
|
||||
config()->set('cdn.files_url', 'https://files.local');
|
||||
|
||||
$photography = ContentType::query()->create([
|
||||
'name' => 'Photography',
|
||||
'slug' => 'photography',
|
||||
]);
|
||||
|
||||
Category::query()->create([
|
||||
'content_type_id' => $photography->id,
|
||||
'name' => 'Flowers',
|
||||
'slug' => 'flowers',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'hash' => 'tgaa11223344',
|
||||
'file_name' => 'rose-closeup.jpg',
|
||||
'title' => 'Together Rose Study',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://vision.local/analyze/all' => Http::response([
|
||||
'clip' => [
|
||||
['tag' => 'rose', 'confidence' => 0.96],
|
||||
['tag' => 'flower', 'confidence' => 0.91],
|
||||
],
|
||||
'yolo' => [
|
||||
['label' => 'flower', 'confidence' => 0.79],
|
||||
],
|
||||
'blip' => 'a close up photograph of a rose bud with soft natural background',
|
||||
], 200),
|
||||
'https://api.together.xyz/v1/chat/completions' => Http::response([
|
||||
'choices' => [
|
||||
[
|
||||
'message' => [
|
||||
'content' => json_encode([
|
||||
'rose macro',
|
||||
'flower close-up',
|
||||
'soft petals',
|
||||
'natural light',
|
||||
'botanical photography',
|
||||
'pink tones',
|
||||
'shallow depth',
|
||||
'floral detail',
|
||||
'macro photography',
|
||||
'garden bloom',
|
||||
], JSON_THROW_ON_ERROR),
|
||||
],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$this->artisan('artworks:ai-suggest', [
|
||||
'artwork_id' => $artwork->id,
|
||||
'--provider' => 'together',
|
||||
])
|
||||
->expectsOutputToContain('provider: together')
|
||||
->expectsOutputToContain('tags: rose-macro, flower-close-up')
|
||||
->assertSuccessful();
|
||||
|
||||
$assist = ArtworkAiAssist::query()->where('artwork_id', $artwork->id)->first();
|
||||
|
||||
expect($assist)->not->toBeNull();
|
||||
expect($assist?->raw_response_json['tag_generation']['provider'] ?? null)->toBe('together');
|
||||
expect($assist?->raw_response_json['tag_generation']['endpoint'] ?? null)->toBe('https://api.together.xyz/v1/chat/completions');
|
||||
|
||||
Http::assertSent(function (\Illuminate\Http\Client\Request $request): bool {
|
||||
return $request->url() === 'https://api.together.xyz/v1/chat/completions'
|
||||
&& $request->hasHeader('Authorization', 'Bearer together-test-key');
|
||||
});
|
||||
});
|
||||
17
tests/Feature/Console/MeilisearchConfigurationTest.php
Normal file
17
tests/Feature/Console/MeilisearchConfigurationTest.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
it('artworks scout index settings include maturity filter fields used by search filters', function () {
|
||||
$indexName = (string) config('scout.prefix', '') . 'artworks';
|
||||
$settings = config('scout.meilisearch.index-settings', []);
|
||||
|
||||
expect($settings)->toBeArray();
|
||||
expect($settings)->toHaveKey($indexName);
|
||||
|
||||
$filterableAttributes = $settings[$indexName]['filterableAttributes'] ?? [];
|
||||
|
||||
expect($filterableAttributes)->toContain('is_mature');
|
||||
expect($filterableAttributes)->toContain('is_mature_effective');
|
||||
expect($filterableAttributes)->toContain('maturity_level');
|
||||
expect($filterableAttributes)->toContain('maturity_status');
|
||||
expect($filterableAttributes)->toContain('published_as_type');
|
||||
});
|
||||
Reference in New Issue
Block a user