generate([], ImageStyle::Cinematic))->toBeNull(); Image::assertNothingGenerated(); }); test('generate returns bytes plus the resolved provider and model when AI succeeds', function () { $bytes = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='); Image::fake([base64_encode($bytes)]); $client = new AiImageClient; $result = $client->generate(['kitchen', 'morning'], ImageStyle::Illustration); expect($result) ->not->toBeNull() ->and($result['bytes'])->toBe($bytes) ->and($result['provider'])->toBe('openai') ->and($result['model'])->toBe('gpt-image-2'); }); test('generate honours AI_IMAGE_PROVIDER instead of always using OpenAI', function () { config()->set('ai.default_for_images', 'gemini'); Image::fake(); $client = new AiImageClient; $result = $client->generate(['kitchen'], ImageStyle::Illustration); expect($result) ->not->toBeNull() ->and($result['provider'])->toBe('gemini'); }); test('generate uses style-specific prompt prefix', function () { Image::fake(); $client = new AiImageClient; $client->generate(['mountain hiker'], ImageStyle::Cinematic); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Cinematic photograph') && $prompt->contains('mountain hiker')); }); test('generate maps orientation to portrait', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, orientation: 'portrait'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->isPortrait()); }); test('generate maps orientation to landscape', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, orientation: 'landscape'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->isLandscape()); }); test('generate falls back to square for unknown orientation', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, orientation: 'whatever'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->isSquare()); }); test('generate appends Brazilian Portuguese instruction when language is pt-BR', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, language: 'pt-BR'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Brazilian Portuguese')); }); test('generate appends Spanish instruction when language is es', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, language: 'es'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Spanish')); }); test('generate appends French instruction when language is fr', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, language: 'fr'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('French')); }); test('generate appends Ukrainian instruction when language is uk', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, language: 'uk'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Ukrainian')); }); test('generate defaults to English instruction when language is unsupported', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, language: 'sv'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('English')); }); test('generate appends brand palette when workspace colours are provided', function () { Image::fake(); $client = new AiImageClient; $client->generate( ['x'], ImageStyle::Infographic, brandColor: '#facc15', backgroundColor: '#ffffff', textColor: '#0f172a', ); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('BRAND COLOR PALETTE') && $prompt->contains('golden yellow') && $prompt->contains('charts, bars') && $prompt->contains('off-white') && $prompt->contains('in-scene typography')); }); test('generate omits brand palette when no workspace colours are set', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic); Image::assertGenerated(fn (ImagePrompt $prompt) => ! $prompt->contains('BRAND COLOR PALETTE')); }); test('generate includes only valid colours in the palette', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, brandColor: 'not-a-hex', backgroundColor: '#ffffff'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('BRAND COLOR PALETTE') && $prompt->contains('off-white') && ! $prompt->contains('Brand / primary accent')); }); test('generate appends brand context when brandDescription is provided', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, brandDescription: 'a fitness coaching brand for busy professionals'); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Brand context') && $prompt->contains('fitness coaching')); }); test('generate truncates brand description longer than 200 chars', function () { Image::fake(); $longDescription = str_repeat('lorem ipsum ', 50); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, brandDescription: $longDescription); Image::assertGenerated(fn (ImagePrompt $prompt) => $prompt->contains('Brand context') && $prompt->contains('…')); }); test('generate omits brand context when brandDescription is empty or whitespace', function () { Image::fake(); $client = new AiImageClient; $client->generate(['x'], ImageStyle::Cinematic, brandDescription: ' '); Image::assertGenerated(fn (ImagePrompt $prompt) => ! $prompt->contains('Brand context')); }); test('generate returns null when SDK throws', function () { Image::fake(fn () => throw new RuntimeException('boom')); $client = new AiImageClient; expect($client->generate(['x'], ImageStyle::Cinematic))->toBeNull(); }); test('generate returns null instead of throwing when the provider responds with no images', function () { Image::fake(fn () => new ImageResponse( new Collection, new Usage, new Meta('openai', 'gpt-image-2'), )); $client = new AiImageClient; expect($client->generate(['x'], ImageStyle::Cinematic))->toBeNull(); });