create(); Livewire::test(Show::class, ['videoProject' => $project]) ->call('startDefault'); expect($project->fresh()->status)->toBe('in_progress'); expect($project->steps()->count())->toBe(8); expect($project->steps()->orderBy('order')->first()->type)->toBe('script_generation'); Queue::assertPushed(RunWorkflowStepJob::class, function ($job) use ($project) { $step = $project->steps()->orderBy('order')->first(); return $job->stepId === $step->id; }); }); test('starting from a template uses its steps and does not queue unhandled types', function () { Queue::fake(); $template = WorkflowTemplate::factory()->create(['steps' => [ ['type' => 'video_generation'], ['type' => 'youtube_publish'], ]]); $project = VideoProject::factory()->create(); Livewire::test(Show::class, ['videoProject' => $project]) ->set('workflowTemplateId', $template->id) ->call('startWithTemplate'); expect($project->fresh()->workflow_template_id)->toBe($template->id); expect($project->steps()->count())->toBe(2); // video_generation has no handler yet, so nothing should be queued Queue::assertNotPushed(RunWorkflowStepJob::class); }); test('startWithTemplate shows a notice when no template is selected', function () { $project = VideoProject::factory()->create(); Livewire::test(Show::class, ['videoProject' => $project]) ->call('startWithTemplate') ->assertSee('Pick a template first.'); expect($project->fresh()->status)->toBe('draft'); }); test('runStep dispatches the job for the given step', function () { Queue::fake(); $project = VideoProject::factory()->create(); $step = (new WorkflowEngine)->start($project, [['type' => 'script_generation']]); Livewire::test(Show::class, ['videoProject' => $project]) ->call('runStep', $step->id); Queue::assertPushed(RunWorkflowStepJob::class, fn ($job) => $job->stepId === $step->id); }); test('approving a step advances and queues the next handled step', function () { Queue::fake(); $project = VideoProject::factory()->create(); $engine = new WorkflowEngine; $step = $engine->start($project, [ ['type' => 'script_generation', 'requires_approval' => true], ['type' => 'tts_voiceover'], ]); $engine->completeStep($step, ['script' => 'draft']); Livewire::test(Show::class, ['videoProject' => $project]) ->call('approveStep', $step->id); $next = $project->steps()->where('order', 1)->first(); expect($next->status)->toBe('in_progress'); Queue::assertPushed(RunWorkflowStepJob::class, fn ($job) => $job->stepId === $next->id); }); test('rejecting a step blocks the pipeline', function () { $project = VideoProject::factory()->create(); $engine = new WorkflowEngine; $step = $engine->start($project, [['type' => 'script_generation', 'requires_approval' => true]]); $engine->completeStep($step, ['script' => 'bad']); Livewire::test(Show::class, ['videoProject' => $project]) ->call('rejectStep', $step->id); expect($step->fresh()->status)->toBe('rejected'); }); test('retrying a failed step re-queues it', function () { Queue::fake(); $project = VideoProject::factory()->create(); $engine = new WorkflowEngine; $step = $engine->start($project, [['type' => 'script_generation']]); $engine->failStep($step, 'boom'); Livewire::test(Show::class, ['videoProject' => $project]) ->call('retryStep', $step->id); expect($step->fresh()->status)->toBe('in_progress'); Queue::assertPushed(RunWorkflowStepJob::class, fn ($job) => $job->stepId === $step->id); }); test('skipping a step advances and queues the next handled step', function () { Queue::fake(); $project = VideoProject::factory()->create(); $engine = new WorkflowEngine; $step = $engine->start($project, [ ['type' => 'video_generation'], ['type' => 'script_generation'], ]); Livewire::test(Show::class, ['videoProject' => $project]) ->call('skipStep', $step->id); $next = $project->steps()->where('order', 1)->first(); expect($step->fresh()->status)->toBe('skipped'); expect($next->status)->toBe('in_progress'); Queue::assertPushed(RunWorkflowStepJob::class, fn ($job) => $job->stepId === $next->id); });