trypost/tests/Unit/Ai/Agents/PostContentGeneratorTest.php
Axi 3baf2e9c41
Add Ukrainian as a supported platform language (#219)
* Default AI content language to Ukrainian

* Keep English as the default content language

* refactor: update content language handling in workspace creation

- Changed the default content language in CreateWorkspace to inherit the app's locale instead of defaulting to English.
- Updated related tests to reflect this change, ensuring that the content language aligns with the application's current locale settings.
- Cleaned up unnecessary code in the BrandTab component for better readability.

* Add Ukrainian as a full platform UI locale.

Wire uk into languages config with complete lang/uk translations and restore ContentLanguage↔UI parity checks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Wire Ukrainian into dayjs and locale coverage tests.

Import the uk dayjs locale with Monday week-start and cover uk/uk-UA in brand autofill and SetLocale assertions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden Ukrainian UI and AI content-language coverage.

Cover generator/reviewer/humanizer/image prompts, workspace pickers, persona labels, UI locale switch, and README for uk as a first-class platform language.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Ukrainian Pinterest form strings after main merge.

Restore lang/uk posts.php key parity for the new pin title and destination link fields.

Co-authored-by: Cursor <cursoragent@cursor.com>

* List every supported UI language in the README.

Replace the abbreviated multi-language blurb with the full locale set from config.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:45:30 -03:00

201 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
use App\Ai\Agents\PostContentGenerator;
use App\Enums\Ai\GeneratorFormat;
use App\Enums\Workspace\ContentLanguage;
use App\Models\Workspace;
use Illuminate\JsonSchema\JsonSchemaTypeFactory;
test('instructions use English for the default content language', function () {
$workspace = Workspace::factory()->make([
'content_language' => ContentLanguage::DEFAULT->value,
]);
expect((new PostContentGenerator(workspace: $workspace))->instructions())
->toContain('Write the output in the language with code: en.');
});
test('instructions use Ukrainian when the workspace content language is uk', function () {
$workspace = Workspace::factory()->make([
'content_language' => ContentLanguage::Ukrainian->value,
]);
expect((new PostContentGenerator(workspace: $workspace))->instructions())
->toContain('Write the output in the language with code: uk.');
});
test('instructions render brand context', function () {
$workspace = Workspace::factory()->make([
'name' => 'TryPost',
'brand_description' => 'Social media scheduling tool',
'brand_voice_traits' => ['friendly', 'direct'],
'content_language' => 'en',
]);
$agent = new PostContentGenerator(workspace: $workspace);
$instructions = $agent->instructions();
expect($instructions)->toContain('TryPost');
expect($instructions)->toContain('friendly'); // "Be warm and friendly."
expect($instructions)->toContain('direct'); // "Use direct, plain, accessible language."
expect($instructions)->toContain('en');
});
test('instructions omit brand description and voice when applyBrandVoice is false', function () {
$workspace = Workspace::factory()->make([
'name' => 'TryPost',
'brand_description' => 'A social scheduling tool for founders',
'brand_voice_traits' => ['first_person', 'transparent'],
'content_language' => 'en',
]);
$agent = new PostContentGenerator(workspace: $workspace, applyBrandVoice: false);
$instructions = $agent->instructions();
expect($instructions)->not->toContain('A social scheduling tool for founders');
expect($instructions)->not->toContain('first person');
expect($instructions)->not->toContain('behind-the-scenes');
// Language still applies — it isn't part of the brand voice.
expect($instructions)->toContain('en');
});
test('instructions include current_content when provided', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(
workspace: $workspace,
currentContent: 'Hello world',
);
expect($agent->instructions())->toContain('Hello world');
});
test('instructions omit current_content when not provided', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace);
expect($agent->instructions())->not->toContain('user already has this content');
});
test('single format schema returns content and image_keywords', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
$schemaFactory = new JsonSchemaTypeFactory;
$schema = $agent->schema($schemaFactory);
expect($schema)->toHaveKey('content');
expect($schema)->toHaveKey('image_keywords');
expect($schema)->not->toHaveKey('slides');
expect($schema)->not->toHaveKey('caption');
});
test('carousel format schema returns caption and slides', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 5);
$schemaFactory = new JsonSchemaTypeFactory;
$schema = $agent->schema($schemaFactory);
expect($schema)->toHaveKey('caption');
expect($schema)->toHaveKey('slides');
expect($schema)->not->toHaveKey('content');
});
test('carousel slide schema includes role enum', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 4);
$schemaFactory = new JsonSchemaTypeFactory;
$schema = $agent->schema($schemaFactory);
$slidesArray = $schema['slides']->toArray();
$slideItem = $slidesArray['items'];
expect($slideItem['properties'])->toHaveKey('role');
expect($slideItem['properties']['role']['enum'])->toBe(['hook', 'development', 'proof', 'cta']);
expect($slideItem['required'])->toContain('role');
});
test('carousel instructions mention slide count', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 5);
$instructions = $agent->instructions();
expect($instructions)->toContain('5');
expect($instructions)->toContain('slides');
});
test('carousel instructions describe the roteiro arc with hook, development, proof and cta', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 4);
$instructions = $agent->instructions();
expect($instructions)->toContain('hook');
expect($instructions)->toContain('development');
expect($instructions)->toContain('proof');
expect($instructions)->toContain('cta');
expect($instructions)->toContain('next action');
});
test('single format instructions do not include carousel roteiro section', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
$instructions = $agent->instructions();
expect($instructions)->not->toContain('Carousel script');
expect($instructions)->not->toContain('Role distribution by slide count');
});
test('single instructions mention image_keywords', function () {
$workspace = Workspace::factory()->make();
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
$instructions = $agent->instructions();
expect($instructions)->toContain('image_keywords');
});
test('instructions include examples when platform context provided and templates exist', function () {
$agent = new PostContentGenerator(
workspace: Workspace::factory()->make(),
format: GeneratorFormat::Carousel,
slideCount: 3,
platformContext: 'instagram_carousel',
);
// The registry ships with carousel templates whose content contains
// distinctive snippets like "Swipe to see" — at least one should appear.
$instructions = $agent->instructions();
expect(
str_contains($instructions, 'Swipe')
|| str_contains($instructions, 'carousel')
|| str_contains($instructions, '{{brand_name}}'),
)->toBeTrue();
});
test('instructions do not include examples section when platform context is null', function () {
$agent = new PostContentGenerator(
workspace: Workspace::factory()->make(),
format: GeneratorFormat::Single,
);
expect($agent->instructions())->not->toContain('curated library');
});
test('instructions inject the platform character cap from the shared budget', function () {
$agent = new PostContentGenerator(
workspace: Workspace::factory()->make(),
format: GeneratorFormat::Single,
platformContext: 'x_post',
);
$instructions = $agent->instructions();
expect($instructions)->toContain('280'); // X hard cap
expect($instructions)->toContain('Hard cap');
});