trypost/tests/Feature/Automation/Run/FanOutTest.php
Paulo Castellano 448ae73389 Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
  graph-aware (suggests only what upstream nodes provide + variables + now);
  migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
  (matching the sidebar card), with a minimize control; the inline field
  collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
  buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form

HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
  dates; first poll records a baseline and emits nothing (date path too)

Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
  so the test panel aggregates all branches instead of one

Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 11:31:58 -03:00

89 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Actions\Automation\Run\AdvanceAutomationRun;
use App\Jobs\Automation\ProcessAutomationNode;
use App\Models\Automation;
use App\Models\AutomationRun;
use App\Models\Post;
use Illuminate\Support\Facades\Bus;
it('fans out to all targets — continues the run on the first branch and forks a sibling for each other', function () {
Bus::fake();
$automation = Automation::factory()->create([
'nodes' => [
['id' => 't', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'schedule']],
['id' => 'a', 'type' => 'end', 'position' => ['x' => 1, 'y' => 0], 'data' => []],
['id' => 'b', 'type' => 'end', 'position' => ['x' => 1, 'y' => 1], 'data' => []],
['id' => 'c', 'type' => 'end', 'position' => ['x' => 1, 'y' => 2], 'data' => []],
],
'connections' => [
['id' => 'e1', 'source' => 't', 'target' => 'a'],
['id' => 'e2', 'source' => 't', 'target' => 'b'],
['id' => 'e3', 'source' => 't', 'target' => 'c'],
],
]);
$run = AutomationRun::factory()->for($automation)->create(['context' => ['shared' => 'value']]);
app(AdvanceAutomationRun::class)($run, 't');
// One job per branch (the original run + two forked siblings).
Bus::assertDispatchedTimes(ProcessAutomationNode::class, 3);
// Two sibling runs created (original + 2), each sharing the parent context.
$runs = AutomationRun::where('automation_id', $automation->id)->get();
expect($runs)->toHaveCount(3);
$runs->each(fn ($r) => expect($r->context)->toBe(['shared' => 'value']));
});
it('forks siblings that inherit the generated post, so a branch after Generate can publish', function () {
Bus::fake();
$post = Post::factory()->create();
$automation = Automation::factory()->create([
'nodes' => [
['id' => 'g', 'type' => 'generate', 'position' => ['x' => 0, 'y' => 0], 'data' => []],
['id' => 'p', 'type' => 'publish', 'position' => ['x' => 1, 'y' => 0], 'data' => ['mode' => 'now']],
['id' => 'w', 'type' => 'webhook', 'position' => ['x' => 1, 'y' => 1], 'data' => []],
],
'connections' => [
['id' => 'e1', 'source' => 'g', 'target' => 'p'],
['id' => 'e2', 'source' => 'g', 'target' => 'w'],
],
]);
$run = AutomationRun::factory()->for($automation)->create([
'current_node_id' => 'g',
'generated_post_id' => $post->id,
]);
app(AdvanceAutomationRun::class)($run, 'g');
$sibling = AutomationRun::where('automation_id', $automation->id)
->whereKeyNot($run->id)
->sole();
expect($sibling->generated_post_id)->toBe($post->id);
});
it('completes the run when a handle has no connected target', function () {
Bus::fake();
$automation = Automation::factory()->create([
'nodes' => [
['id' => 't', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'schedule']],
],
'connections' => [],
]);
$run = AutomationRun::factory()->for($automation)->create();
app(AdvanceAutomationRun::class)($run, 't');
expect($run->fresh()->status->value)->toBe('completed');
Bus::assertNotDispatched(ProcessAutomationNode::class);
});