feat: save ai usage
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Ai\Agents\MealMaker;
|
||||
use App\Enums\AiUsageStatus;
|
||||
use App\Enums\AiUsageType;
|
||||
use App\Enums\IngredientUnit;
|
||||
use App\Enums\MealPostType;
|
||||
use App\Enums\MealPostVisibility;
|
||||
use App\Models\AiUsage;
|
||||
use App\Models\MealPosts;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Exceptions;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Ai\Image;
|
||||
|
||||
@@ -119,4 +123,80 @@ it('generates an ai meal post with an image', function () {
|
||||
&& $prompt->isLandscape()
|
||||
&& $prompt->quality === 'medium'
|
||||
);
|
||||
|
||||
$usage = AiUsage::query()->sole();
|
||||
|
||||
expect($usage->user_id)->toBe($mealPost->user_id)
|
||||
->and($usage->type)->toBe(AiUsageType::MEAL_POST_GENERATION)
|
||||
->and($usage->status)->toBe(AiUsageStatus::SUCCESS)
|
||||
->and($usage->related_type)->toBe(MealPosts::class)
|
||||
->and($usage->related_id)->toBe($mealPost->id)
|
||||
->and($usage->input['image'])->toMatchArray([
|
||||
'size' => '3:2',
|
||||
'quality' => 'medium',
|
||||
'storage_path' => 'meal-posts/ai-generated',
|
||||
])
|
||||
->and($usage->output)->toMatchArray([
|
||||
'title' => 'Bowl de saumon croustillant',
|
||||
'type' => MealPostType::LUNCH->value,
|
||||
'calories' => 640,
|
||||
'image_url' => $mealPost->image_url,
|
||||
'ingredients' => [
|
||||
[
|
||||
'position' => 1,
|
||||
'ingredient' => 'Riz vinaigre',
|
||||
'quantity' => 160,
|
||||
'unit' => IngredientUnit::GRAM->value,
|
||||
],
|
||||
[
|
||||
'position' => 2,
|
||||
'ingredient' => 'Saumon',
|
||||
'quantity' => 130,
|
||||
'unit' => IngredientUnit::GRAM->value,
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('records a failed ai meal post generation attempt', function () {
|
||||
Storage::fake('public');
|
||||
Exceptions::fake();
|
||||
|
||||
config()->set('meal_posts.ai_generation.user_name', 'Chef IA');
|
||||
config()->set('meal_posts.ai_generation.user_email', 'chef-ia@example.test');
|
||||
config()->set('meal_posts.ai_generation.image_quality', 'medium');
|
||||
|
||||
MealMaker::fake([
|
||||
[
|
||||
'title' => 'Bowl impossible',
|
||||
'caption' => 'Un bowl qui ne sera pas publie.',
|
||||
'type' => MealPostType::LUNCH->value,
|
||||
'calories' => 640,
|
||||
'proteins' => 42.4,
|
||||
'carbs' => 62.2,
|
||||
'fats' => 24.8,
|
||||
'ingredients' => [],
|
||||
'image_prompt' => 'Photorealistic impossible bowl',
|
||||
],
|
||||
]);
|
||||
|
||||
Image::fake(
|
||||
fn () => throw new \RuntimeException('Image provider unavailable')
|
||||
);
|
||||
|
||||
$this->artisan('meal-posts:generate-ai')
|
||||
->assertExitCode(1);
|
||||
|
||||
Exceptions::assertReported(\RuntimeException::class);
|
||||
|
||||
expect(MealPosts::query()->where('ai_generated', true)->count())->toBe(0);
|
||||
|
||||
$usage = AiUsage::query()->sole();
|
||||
|
||||
expect($usage->user->email)->toBe('chef-ia@example.test')
|
||||
->and($usage->type)->toBe(AiUsageType::MEAL_POST_GENERATION)
|
||||
->and($usage->status)->toBe(AiUsageStatus::FAILED)
|
||||
->and($usage->input['image_prompt'])->toContain('impossible bowl')
|
||||
->and($usage->output)->toBeNull()
|
||||
->and($usage->error_message)->toContain('Image provider unavailable');
|
||||
});
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<?php
|
||||
|
||||
use App\Ai\Agents\MealImageAnalyzer;
|
||||
use App\Enums\AiUsageStatus;
|
||||
use App\Enums\AiUsageType;
|
||||
use App\Enums\IngredientUnit;
|
||||
use App\Enums\MealPostType;
|
||||
use App\Models\AiUsage;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Exceptions;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@@ -22,7 +26,9 @@ function fakeMealAnalysisImage(): UploadedFile
|
||||
}
|
||||
|
||||
it('analyzes a meal image into a draft', function () {
|
||||
Sanctum::actingAs(User::factory()->create());
|
||||
$user = User::factory()->create();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
MealImageAnalyzer::fake([
|
||||
[
|
||||
@@ -70,6 +76,69 @@ it('analyzes a meal image into a draft', function () {
|
||||
fn ($prompt): bool => str_contains($prompt->prompt, "Analyse l'image")
|
||||
&& $prompt->attachments->count() === 1
|
||||
);
|
||||
|
||||
$usage = AiUsage::query()->sole();
|
||||
|
||||
expect($usage->user_id)->toBe($user->id)
|
||||
->and($usage->type)->toBe(AiUsageType::MEAL_IMAGE_ANALYSIS)
|
||||
->and($usage->status)->toBe(AiUsageStatus::SUCCESS)
|
||||
->and($usage->input['image'])->toMatchArray([
|
||||
'client_original_name' => 'meal.png',
|
||||
'mime_type' => 'image/png',
|
||||
])
|
||||
->and($usage->output)->toMatchArray([
|
||||
'title' => 'Bowl saumon avocat',
|
||||
'type' => MealPostType::LUNCH->value,
|
||||
'calories' => 612,
|
||||
'proteins' => 39.4,
|
||||
'ingredients' => [
|
||||
[
|
||||
'position' => 1,
|
||||
'ingredient' => 'Riz',
|
||||
'quantity' => 150,
|
||||
'unit' => IngredientUnit::GRAM->value,
|
||||
],
|
||||
[
|
||||
'position' => 2,
|
||||
'ingredient' => 'Saumon',
|
||||
'quantity' => 120,
|
||||
'unit' => IngredientUnit::GRAM->value,
|
||||
],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it('records a failed meal image analysis attempt', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
Exceptions::fake();
|
||||
|
||||
MealImageAnalyzer::fake(
|
||||
fn () => throw new \RuntimeException('Provider unavailable')
|
||||
);
|
||||
|
||||
$this
|
||||
->withHeader('Accept', 'application/json')
|
||||
->post('/api/meal-posts/analyze-image', [
|
||||
'image' => fakeMealAnalysisImage(),
|
||||
])
|
||||
->assertStatus(502)
|
||||
->assertJsonPath('message', __('api.meal_image_analysis.failed'));
|
||||
|
||||
Exceptions::assertReported(\RuntimeException::class);
|
||||
|
||||
$usage = AiUsage::query()->sole();
|
||||
|
||||
expect($usage->user_id)->toBe($user->id)
|
||||
->and($usage->type)->toBe(AiUsageType::MEAL_IMAGE_ANALYSIS)
|
||||
->and($usage->status)->toBe(AiUsageStatus::FAILED)
|
||||
->and($usage->input['image'])->toMatchArray([
|
||||
'client_original_name' => 'meal.png',
|
||||
'mime_type' => 'image/png',
|
||||
])
|
||||
->and($usage->output)->toBeNull()
|
||||
->and($usage->error_message)->toContain('Provider unavailable');
|
||||
});
|
||||
|
||||
it('requires authentication to analyze a meal image', function () {
|
||||
|
||||
Reference in New Issue
Block a user