'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); expect($post->created_via)->toBe(CreatedVia::Automation); }); it('applies brand voice by default', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $run = AutomationRun::factory()->create(['context' => ['trigger' => ['title' => 'News']]]); app(RunGenerateNode::class)($run, [ 'accounts' => [], 'prompt_template' => 'Write about {{ trigger.title }}', ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->applyBrandVoice === true); PostContentHumanizer::assertPrompted(fn ($prompt) => $prompt->agent->applyBrandVoice === true); }); it('skips brand voice when use_brand_voice is off', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $run = AutomationRun::factory()->create(['context' => ['trigger' => ['title' => 'OpenAI news']]]); app(RunGenerateNode::class)($run, [ 'accounts' => [], 'prompt_template' => 'Write about {{ trigger.title }}', 'use_brand_voice' => false, ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->applyBrandVoice === false); PostContentHumanizer::assertPrompted(fn ($prompt) => $prompt->agent->applyBrandVoice === false); }); it('passes the selected network as the generator platform context', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'linkedin']); $run = AutomationRun::factory()->for(Automation::factory()->for($workspace))->create([ 'context' => ['trigger' => ['title' => 'News']], ]); app(RunGenerateNode::class)($run, [ 'accounts' => [['social_account_id' => (string) $account->id, 'content_type' => ContentType::LinkedInPost->value]], 'prompt_template' => 'Write about {{ trigger.title }}', ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->platformContext === ContentType::LinkedInPost->value); }); it('writes for the most restrictive network when several are selected', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $workspace = Workspace::factory()->create(); $linkedin = SocialAccount::factory()->for($workspace)->create(['platform' => 'linkedin']); $x = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $run = AutomationRun::factory()->for(Automation::factory()->for($workspace))->create([ 'context' => ['trigger' => ['title' => 'News']], ]); // X caps at 280 chars, LinkedIn at 3000 — copy must fit the tighter one. app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $linkedin->id, 'content_type' => ContentType::LinkedInPost->value], ['social_account_id' => (string) $x->id, 'content_type' => ContentType::XPost->value], ], 'prompt_template' => 'Write about {{ trigger.title }}', ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->platformContext === ContentType::XPost->value); // The humanizer second pass gets the same context so it can't rewrite the // copy back over the platform's character cap. PostContentHumanizer::assertPrompted(fn ($prompt) => $prompt->agent->platformContext === ContentType::XPost->value); }); it('leaves the platform context null when no account carries a content type', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $run = AutomationRun::factory()->for(Automation::factory()->for($workspace))->create([ 'context' => ['trigger' => ['title' => 'News']], ]); app(RunGenerateNode::class)($run, [ 'accounts' => [['social_account_id' => (string) $account->id]], 'prompt_template' => 'Write about {{ trigger.title }}', ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->platformContext === null); }); it('still resolves the most restrictive context when the format is a carousel', function () { PostContentGenerator::fake([ ['caption' => 'cap', 'slides' => [ ['title' => 'S1', 'body' => 'B1', 'image_keywords' => ['a']], ['title' => 'S2', 'body' => 'B2', 'image_keywords' => ['b']], ]], ]); PostContentHumanizer::fake([ ['caption' => 'cap', 'slides' => [['title' => 'S1', 'body' => 'B1'], ['title' => 'S2', 'body' => 'B2']]], ]); $workspace = Workspace::factory()->create(); $instagram = SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram']); $x = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $run = AutomationRun::factory()->for(Automation::factory()->for($workspace))->create([ 'context' => ['trigger' => ['title' => 'News']], ]); // Instagram feed is carousel-capable, so the format is a carousel, yet the // platform context must still be X (280) — the tightest selected network. app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $instagram->id, 'content_type' => ContentType::InstagramFeed->value], ['social_account_id' => (string) $x->id, 'content_type' => ContentType::XPost->value], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 2, ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->format === GeneratorFormat::Carousel && $prompt->agent->platformContext === ContentType::XPost->value); }); it('leaves the platform context null for the legacy social_account_ids shape', function () { PostContentGenerator::fake([ ['content' => 'c', 'image_title' => 't', 'image_body' => 'b', 'image_keywords' => ['k']], ]); PostContentHumanizer::fake([['content' => 'c', 'image_title' => 't', 'image_body' => 'b']]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $run = AutomationRun::factory()->for(Automation::factory()->for($workspace))->create([ 'context' => ['trigger' => ['title' => 'News']], ]); app(RunGenerateNode::class)($run, [ 'social_account_ids' => [(string) $account->id], 'prompt_template' => 'Write about {{ trigger.title }}', ]); PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->platformContext === null); }); 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' => ContentType::InstagramFeed->value, 'meta' => []], ]; ['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]); expect($format)->toBe(GeneratorFormat::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' => ContentType::InstagramFeed->value, 'meta' => []], ]; ['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 1]); expect($format)->toBe(GeneratorFormat::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' => ContentType::PinterestPin->value, 'meta' => []], ['social_account_id' => '2', 'content_type' => ContentType::InstagramReel->value, 'meta' => []], ]; ['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]); expect($format)->toBe(GeneratorFormat::Single); expect($slideCount)->toBe(1); }); it('attaches a generated image to a single-format post', function () { PostContentGenerator::fake([ ['content' => 'Single post', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']], ]); PostContentHumanizer::fake([ ['content' => 'Single post', 'image_title' => 'Title', 'image_body' => 'Body'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $mediaItem = ['id' => 1, 'path' => 'ai-images/x.webp', 'url' => 'http://x', 'type' => 'image', 'mime_type' => 'image/webp', 'source' => 'ai', 'source_meta' => []]; $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forSingle')->once()->andReturn([$mediaItem]); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->media)->toHaveCount(1); }); it('attaches one image per slide for carousel', function () { PostContentGenerator::fake([ ['caption' => 'Carousel caption', 'slides' => [ ['title' => 'S1', 'body' => 'B1', 'image_keywords' => ['a']], ['title' => 'S2', 'body' => 'B2', 'image_keywords' => ['b']], ['title' => 'S3', 'body' => 'B3', 'image_keywords' => ['c']], ]], ]); PostContentHumanizer::fake([ ['caption' => 'Carousel caption', 'slides' => [ ['title' => 'S1', 'body' => 'B1'], ['title' => 'S2', 'body' => 'B2'], ['title' => 'S3', 'body' => 'B3'], ]], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'instagram']); $item = fn (int $id) => ['id' => $id, 'path' => "ai-images/{$id}.webp", 'url' => "http://{$id}", 'type' => 'image', 'mime_type' => 'image/webp', 'source' => 'ai', 'source_meta' => []]; $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forCarousel')->once()->andReturn([$item(1), $item(2), $item(3)]); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::InstagramFeed->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 3, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->media)->toHaveCount(3); }); it('skips images when image count is zero', function () { PostContentGenerator::fake([ ['content' => 'No image post', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']], ]); PostContentHumanizer::fake([ ['content' => 'No image post', 'image_title' => 'Title', 'image_body' => 'Body'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldNotReceive('forSingle'); $pipeline->shouldNotReceive('forCarousel'); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 0, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->media)->toHaveCount(0); }); it('does not generate images or persist on a dry run', function () { PostContentGenerator::fake([ ['content' => 'Dry run post', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']], ]); PostContentHumanizer::fake([ ['content' => 'Dry run post', 'image_title' => 'Title', 'image_body' => 'Body'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldNotReceive('forSingle'); $pipeline->shouldNotReceive('forCarousel'); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'is_dry_run' => true, 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); expect($result->output['generated']['dry_run'])->toBeTrue(); expect($result->output['generated']['content'])->toBe('Dry run post'); expect($result->output['generated']['image_count'])->toBe(1); expect($result->output['generated']['post_id'])->toBeNull(); expect(Post::count())->toBe(0); expect(Media::count())->toBe(0); $run->refresh(); expect($run->generated_post_id)->toBeNull(); }); it('defaults to image_card style when no style is set, behaving identically to before', function () { PostContentGenerator::fake([ ['content' => 'Legacy post', 'image_title' => 'Title', 'image_body' => 'Body', 'image_keywords' => ['kw']], ]); PostContentHumanizer::fake([ ['content' => 'Legacy post', 'image_title' => 'Title', 'image_body' => 'Body'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $mediaItem = ['id' => 1, 'path' => 'ai-images/x.webp', 'url' => 'http://x', 'type' => 'image', 'mime_type' => 'image/webp', 'source' => 'ai', 'source_meta' => []]; $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forSingle')->once()->andReturn([$mediaItem]); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->content)->toBe('Legacy post'); expect($post->media)->toHaveCount(1); }); it('generates a tweet card post when style is tweet_card', function () { PostContentGenerator::fake([ ['tweet_text' => 'Hot take on AI agents today.'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $mediaItem = ['id' => 2, 'path' => 'ai-images/tweet.webp', 'url' => 'http://tweet', 'type' => 'image', 'mime_type' => 'image/webp', 'source' => 'ai', 'source_meta' => []]; $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forTweetCard')->once()->andReturn([$mediaItem]); $pipeline->shouldNotReceive('forSingle'); $pipeline->shouldNotReceive('forCarousel'); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'style' => ContentStyle::TweetCard->value, 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write a tweet about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->content)->toBe('Hot take on AI agents today.'); expect($post->media)->toHaveCount(1); }); it('skips the humanizer for tweet_card style', function () { PostContentGenerator::fake([ ['tweet_text' => 'Raw tweet, no humanizer.'], ]); PostContentHumanizer::fake([]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forTweetCard')->once()->andReturn([]); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); app(RunGenerateNode::class)($run, [ 'style' => ContentStyle::TweetCard->value, 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write a tweet about {{ trigger.title }}', 'target_slide_count' => 1, ]); PostContentHumanizer::assertNeverPrompted(); }); it('generates a tweet card with AI image background when style is tweet_card_image', function () { PostContentGenerator::fake([ ['tweet_text' => 'AI is transforming everything.', 'image_keywords' => ['artificial intelligence', 'technology']], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $mediaItem = ['id' => 3, 'path' => 'ai-images/tweet-img.webp', 'url' => 'http://tweet-img', 'type' => 'image', 'mime_type' => 'image/webp', 'source' => 'ai', 'source_meta' => []]; $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forTweetCard') ->once() ->withArgs(function ($ws, $acc, $text, $keywords) { return $text === 'AI is transforming everything.' && is_array($keywords) && count($keywords) > 0; }) ->andReturn([$mediaItem]); $pipeline->shouldNotReceive('forSingle'); $pipeline->shouldNotReceive('forCarousel'); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'style' => ContentStyle::TweetCardImage->value, 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write a tweet about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); $post = Post::find($result->output['generated']['post_id']); expect($post)->not->toBeNull(); expect($post->content)->toBe('AI is transforming everything.'); expect($post->media)->toHaveCount(1); }); it('skips the humanizer for tweet_card_image style', function () { PostContentGenerator::fake([ ['tweet_text' => 'Raw tweet with image.', 'image_keywords' => ['tech']], ]); PostContentHumanizer::fake([]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldReceive('forTweetCard')->once()->andReturn([]); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'context' => ['trigger' => ['title' => 'test']], ]); app(RunGenerateNode::class)($run, [ 'style' => ContentStyle::TweetCardImage->value, 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write a tweet about {{ trigger.title }}', 'target_slide_count' => 1, ]); PostContentHumanizer::assertNeverPrompted(); }); it('dry run returns sensible image_count for tweet_card style', function () { PostContentGenerator::fake([ ['tweet_text' => 'A tweet for dry run.'], ]); $workspace = Workspace::factory()->create(); $account = SocialAccount::factory()->for($workspace)->create(['platform' => 'x']); $pipeline = Mockery::mock(PostImagePipeline::class); $pipeline->shouldNotReceive('forTweetCard'); $pipeline->shouldNotReceive('forSingle'); $pipeline->shouldNotReceive('forCarousel'); app()->instance(PostImagePipeline::class, $pipeline); $automation = Automation::factory()->for($workspace)->create(); $run = AutomationRun::factory()->for($automation)->create([ 'is_dry_run' => true, 'context' => ['trigger' => ['title' => 'test']], ]); $result = app(RunGenerateNode::class)($run, [ 'style' => ContentStyle::TweetCard->value, 'accounts' => [ ['social_account_id' => (string) $account->id, 'content_type' => ContentType::XPost->value, 'meta' => []], ], 'prompt_template' => 'Write about {{ trigger.title }}', 'target_slide_count' => 1, ]); expect($result->status->value)->toBe('completed'); expect($result->output['generated']['dry_run'])->toBeTrue(); expect($result->output['generated']['image_count'])->toBe(1); expect($result->output['generated']['post_id'])->toBeNull(); expect(Post::count())->toBe(0); });