Langertha

 view release on metacpan or  search on metacpan

t/96_raid_orchestration.t  view on Meta::CPAN

  is($result->text, 'stage2:yes', 'second step reads state written by first step');
};

subtest 'Parallel context isolation and merge behavior' => sub {
  my $parallel = Langertha::Raid::Parallel->new(steps => [
    Test::Runnable::Step->new(
      name => 'left',
      result_cb => sub {
        my ( $ctx ) = @_;
        $ctx->state->{counter}++;
        $ctx->artifacts->{branch} = 'left';
        return Langertha::Result->final('L');
      },
    ),
    Test::Runnable::Step->new(
      name => 'right',
      result_cb => sub {
        my ( $ctx ) = @_;
        $ctx->state->{counter}++;
        $ctx->artifacts->{branch} = 'right';
        return Langertha::Result->final('R');
      },
    ),
  ]);

  my $ctx = Langertha::RunContext->new(
    input => 'seed',
    state => { counter => 0 },
  );
  my $result = $parallel->run_f($ctx)->get;

  ok($result->is_final, 'parallel returns final when all branches are final');
  is($ctx->state->{counter}, 0, 'parent state was not mutated by branch internals');
  is($result->text, "L\nR", 'branch final texts are aggregated in stable order');
  is(scalar keys %{$ctx->artifacts->{parallel_branches}}, 2, 'both branch contexts merged');
};

subtest 'Parallel propagation and error path' => sub {
  my $aborting = Langertha::Raid::Parallel->new(steps => [
    Test::Runnable::Step->new(name => 'ok', type => 'final', text => 'ok'),
    Test::Runnable::Step->new(name => 'bad', type => 'abort', content => 'stop'),
  ]);
  my $abort_result = $aborting->run_f(Langertha::RunContext->new(input => 'x'))->get;
  ok($abort_result->is_abort, 'abort from a branch aborts the whole parallel raid');

  my $questioning = Langertha::Raid::Parallel->new(steps => [
    Test::Runnable::Step->new(name => 'q', type => 'question', content => 'which one?'),
    Test::Runnable::Step->new(name => 'ok', type => 'final', text => 'ok'),
  ]);
  my $question_result = $questioning->run_f(Langertha::RunContext->new(input => 'x'))->get;
  ok($question_result->is_question, 'question propagates when no abort exists');

  my $dying = Langertha::Raid::Parallel->new(steps => [
    Test::Runnable::Step->new(name => 'explode', die_message => 'parallel boom'),
  ]);
  my $dying_result = $dying->run_f(Langertha::RunContext->new(input => 'x'))->get;
  ok($dying_result->is_abort, 'branch exception becomes abort');
  like($dying_result->content, qr/parallel boom/, 'abort keeps branch failure reason');
};

subtest 'Loop max iterations and stop callback' => sub {
  my $step = Test::Runnable::Step->new(
    name => 'counter',
    result_cb => sub {
      my ( $ctx ) = @_;
      $ctx->state->{n} = ($ctx->state->{n} // 0) + 1;
      return Langertha::Result->final('n=' . $ctx->state->{n});
    },
  );

  my $loop = Langertha::Raid::Loop->new(
    steps     => [$step],
    max_loops => 3,
  );
  my $ctx = Langertha::RunContext->new(input => 'start');
  my $result = $loop->run_f($ctx)->get;
  ok($result->is_final, 'loop finalizes after max_loops');
  is($result->text, 'n=3', 'loop ran exactly three iterations');
  is($ctx->metadata->{loop_iterations}, 3, 'loop stores total iterations');

  my $loop_cb = Langertha::Raid::Loop->new(
    steps          => [$step],
    max_loops      => 10,
    continue_while => sub {
      my ( $ctx, $iteration ) = @_;
      return $iteration < 2;
    },
  );
  my $ctx2 = Langertha::RunContext->new(input => 'start');
  my $result2 = $loop_cb->run_f($ctx2)->get;
  is($result2->text, 'n=2', 'continue_while can stop before max_loops');
  is($ctx2->metadata->{loop_iterations}, 2, 'iteration metadata reflects early stop');
};

subtest 'Loop propagation and error path' => sub {
  my $loop_pause = Langertha::Raid::Loop->new(
    steps     => [Test::Runnable::Step->new(name => 'pause', type => 'pause', content => 'wait')],
    max_loops => 5,
  );
  my $pause_result = $loop_pause->run_f(Langertha::RunContext->new(input => 'x'))->get;
  ok($pause_result->is_pause, 'pause propagates out of loop');

  my $loop_error = Langertha::Raid::Loop->new(
    steps     => [Test::Runnable::Step->new(name => 'boom', die_message => 'loop failed')],
    max_loops => 2,
  );
  my $error_result = $loop_error->run_f(Langertha::RunContext->new(input => 'x'))->get;
  ok($error_result->is_abort, 'loop step exception returns abort');
  like($error_result->content, qr/loop failed/, 'loop abort includes exception');
};

subtest 'Nested raid compositions are supported' => sub {
  my $raider = Test::CompatRaider->new(engine => Test::DummyEngine->new, prefix => 'R:');
  my $parallel = Langertha::Raid::Parallel->new(steps => [
    Test::Runnable::Step->new(name => 'p1', type => 'final', text => 'P1'),
    $raider,
  ]);
  my $loop_with_seq = Langertha::Raid::Loop->new(
    max_loops => 2,
    steps     => [
      Langertha::Raid::Sequential->new(steps => [
        Test::Runnable::Step->new(
          name => 'dot',
          result_cb => sub {
            my ( $ctx ) = @_;
            return Langertha::Result->final(($ctx->input // '') . '.');
          },
        ),
      ]),
    ],
  );

  my $top = Langertha::Raid::Sequential->new(steps => [
    Test::Runnable::Step->new(name => 'start', type => 'final', text => 'S'),
    $parallel,
    $loop_with_seq,
  ]);

  my $result = $top->run_f(Langertha::RunContext->new(input => 'seed'))->get;
  ok($result->is_final, 'nested orchestration returns final');
  like($result->text, qr/\.\.$/, 'loop-with-sequential runs within nested tree');
};

done_testing;



( run in 1.338 second using v1.01-cache-2.11-cpan-71847e10f99 )