- Added new automation-related routes and controllers for managing automations. - Introduced automation nodes in the UI with distinct styles and interactions. - Updated sidebar to include navigation for automations. - Enhanced post creation logic to support automation metadata. - Refactored content type and platform enums into types for better type safety. - Added localization for automation-related terms in English, Spanish, and Portuguese. - Improved error handling in various components to accommodate new features.
132 lines
4.8 KiB
PHP
132 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Actions\Automation\Node\RunGenerateNode;
|
|
use App\Ai\Agents\PostContentGenerator;
|
|
use App\Ai\Agents\PostContentHumanizer;
|
|
use App\Enums\Post\Status as PostStatus;
|
|
use App\Models\AutomationRun;
|
|
use App\Models\Post;
|
|
use App\Models\SocialAccount;
|
|
use App\Models\Workspace;
|
|
|
|
it('creates a draft post and writes generated output to run context', function () {
|
|
PostContentGenerator::fake([
|
|
['content' => 'Great post about Stripe Radar', 'image_title' => 'Stripe Radar', 'image_body' => 'Fraud prevention', 'image_keywords' => ['stripe', 'fraud']],
|
|
]);
|
|
|
|
PostContentHumanizer::fake([
|
|
['content' => 'Great post about Stripe Radar (humanized)', 'image_title' => 'Stripe Radar', 'image_body' => 'Fraud prevention'],
|
|
]);
|
|
|
|
$run = AutomationRun::factory()->create([
|
|
'context' => ['trigger' => ['title' => 'Stripe Radar update']],
|
|
]);
|
|
|
|
$result = app(RunGenerateNode::class)($run, [
|
|
'accounts' => [],
|
|
'prompt_template' => 'Generate a post about: {{ trigger.title }}',
|
|
'image_source' => 'none',
|
|
]);
|
|
|
|
expect($result->status->value)->toBe('completed');
|
|
expect($result->output)->toHaveKey('generated');
|
|
expect($result->output['generated'])->toHaveKey('post_id');
|
|
expect($result->output['generated']['content'])->toBe('Great post about Stripe Radar (humanized)');
|
|
|
|
$run->refresh();
|
|
expect($run->generated_post_id)->not->toBeNull();
|
|
|
|
$post = Post::find($run->generated_post_id);
|
|
expect($post)->not->toBeNull();
|
|
expect($post->status)->toBe(PostStatus::Draft);
|
|
});
|
|
|
|
it('interpolates the prompt template before passing it to the generator', function () {
|
|
PostContentGenerator::fake([
|
|
['content' => 'Interpolated content', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']],
|
|
]);
|
|
|
|
PostContentHumanizer::fake([
|
|
['content' => 'Interpolated content humanized', 'image_title' => 'Title', 'image_body' => 'Body'],
|
|
]);
|
|
|
|
$run = AutomationRun::factory()->create([
|
|
'context' => ['trigger' => ['topic' => 'AI Agents']],
|
|
]);
|
|
|
|
app(RunGenerateNode::class)($run, [
|
|
'accounts' => [],
|
|
'prompt_template' => 'Write about {{ trigger.topic }}',
|
|
]);
|
|
|
|
PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->contains('Write about AI Agents'));
|
|
});
|
|
|
|
it('still creates a draft post when humanizer fails', function () {
|
|
PostContentGenerator::fake([
|
|
['content' => 'Raw content', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']],
|
|
]);
|
|
|
|
PostContentHumanizer::fake(fn () => throw new RuntimeException('Humanizer down'));
|
|
|
|
$run = AutomationRun::factory()->create([
|
|
'context' => ['trigger' => ['title' => 'test']],
|
|
]);
|
|
|
|
$result = app(RunGenerateNode::class)($run, [
|
|
'accounts' => [],
|
|
'prompt_template' => 'Write about {{ trigger.title }}',
|
|
]);
|
|
|
|
expect($result->status->value)->toBe('completed');
|
|
expect($result->output['generated']['content'])->toBe('Raw content');
|
|
|
|
$run->refresh();
|
|
expect($run->generated_post_id)->not->toBeNull();
|
|
});
|
|
|
|
it('derives carousel format when a carousel-capable account is configured with target_slide_count > 1', function () {
|
|
$workspace = Workspace::factory()->create();
|
|
$account = SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram']);
|
|
|
|
$action = app(RunGenerateNode::class);
|
|
|
|
$accountsConfig = [
|
|
['social_account_id' => (string) $account->id, 'content_type' => 'instagram_carousel', 'meta' => []],
|
|
];
|
|
|
|
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]);
|
|
|
|
expect($format)->toBe('carousel');
|
|
expect($slideCount)->toBe(5);
|
|
});
|
|
|
|
it('derives single format when carousel account has target_slide_count of 1', function () {
|
|
$workspace = Workspace::factory()->create();
|
|
$account = SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram']);
|
|
|
|
$action = app(RunGenerateNode::class);
|
|
|
|
$accountsConfig = [
|
|
['social_account_id' => (string) $account->id, 'content_type' => 'instagram_carousel', 'meta' => []],
|
|
];
|
|
|
|
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 1]);
|
|
|
|
expect($format)->toBe('single');
|
|
expect($slideCount)->toBe(1);
|
|
});
|
|
|
|
it('derives single format when no carousel-capable account is configured', function () {
|
|
$action = app(RunGenerateNode::class);
|
|
|
|
$accountsConfig = [
|
|
['social_account_id' => '1', 'content_type' => 'x_post', 'meta' => []],
|
|
['social_account_id' => '2', 'content_type' => 'instagram_feed', 'meta' => []],
|
|
];
|
|
|
|
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]);
|
|
|
|
expect($format)->toBe('single');
|
|
expect($slideCount)->toBe(1);
|
|
});
|