App-karr
view release on metacpan or search on metacpan
t/31-foundation-drain.t view on Meta::CPAN
$t->claimed_by('fake-agent');
$store->save_task($t);
}
# already claimed/in-progress -> do nothing -> no board change -> stall
}
}
elsif ( $mode eq 'error' ) {
print "Error: rate limit exceeded, retry later\n";
exit 0;
}
PERL
return qq{$^X -I"$lib" "$script"};
}
# ---------------------------------------------------------------------------
# Unit: actionability
# ---------------------------------------------------------------------------
subtest '_is_actionable' => sub {
my $f = App::karr::Foundation->new;
ok $f->_is_actionable({ status => 'todo' }), 'todo actionable';
ok $f->_is_actionable({ status => 'in-progress' }), 'in-progress actionable';
ok $f->_is_actionable({ status => 'backlog' }), 'backlog actionable';
ok ! $f->_is_actionable({ status => 'done' }), 'done not actionable';
ok ! $f->_is_actionable({ status => 'archived' }), 'archived not actionable';
ok ! $f->_is_actionable({ status => 'todo', blocked => 1 }), 'blocked not actionable';
ok ! $f->_is_actionable(undef), 'undef not actionable';
};
subtest '_has_actionable_tasks' => sub {
my $repo = make_git_repo();
seed_board( $repo, { status => 'done' }, { status => 'todo', blocked => 'x' } );
my $f = App::karr::Foundation->new;
ok ! $f->_has_actionable_tasks( $repo ), 'done + blocked => none actionable';
seed_board( $repo, { status => 'todo' } );
ok $f->_has_actionable_tasks( $repo ), 'a todo makes it actionable';
};
# ---------------------------------------------------------------------------
# Unit: common-error detection
# ---------------------------------------------------------------------------
subtest 'error patterns' => sub {
my $f = App::karr::Foundation->new;
my $pat = $f->_error_patterns({});
is $f->_match_error( "blah RATE LIMIT hit", $pat ), 'rate limit', 'case-insensitive default';
is $f->_match_error( "all good\n", $pat ), undef, 'clean text => undef';
is $f->_match_error( '', $pat ), undef, 'empty => undef';
my $cp = $f->_error_patterns({ error_patterns => ['kaboom'] });
is $f->_match_error( "something kaboom", $cp ), 'kaboom', 'custom pattern matches';
};
# ---------------------------------------------------------------------------
# Unit: stuck-task detection
# ---------------------------------------------------------------------------
subtest '_stuck_tasks' => sub {
my $f = App::karr::Foundation->new;
my $before = {
1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' },
2 => { status => 'todo', claimed_by => undef, updated => 'T1' },
3 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' },
};
my $after = {
1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' }, # unchanged -> stuck
2 => { status => 'todo', claimed_by => undef, updated => 'T1' }, # not claimed -> ignore
3 => { status => 'done', claimed_by => 'a', updated => 'T2' }, # advanced -> not stuck
};
is_deeply [ $f->_stuck_tasks( $before, $after ) ], [1], 'only the unchanged claimed task is stuck';
# blocked task is never stuck
my $b2 = { 1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' } };
my $a2 = { 1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1', blocked => 1 } };
is_deeply [ $f->_stuck_tasks( $b2, $a2 ) ], [], 'blocked task drops out';
};
# ---------------------------------------------------------------------------
# Unit: attempts counter
# ---------------------------------------------------------------------------
subtest 'attempts counter' => sub {
my $repo = tempdir( CLEANUP => 1 );
my $f = App::karr::Foundation->new;
is $f->_bump_attempts( $repo, 7 ), 1, 'first bump => 1';
is $f->_bump_attempts( $repo, 7 ), 2, 'second bump => 2';
is $f->_state_get( $repo, 'attempts' )->{7}, 2, 'persisted in state';
$f->_reset_attempts( $repo, 7 );
ok ! exists $f->_state_get( $repo, 'attempts' )->{7}, 'reset removes key';
};
# ---------------------------------------------------------------------------
# Unit: exponential cooldown
# ---------------------------------------------------------------------------
subtest 'exponential cooldown' => sub {
my $repo = tempdir( CLEANUP => 1 );
my $f = App::karr::Foundation->new;
my $karr = { cooldown_base => 1, cooldown_max => 4 };
is $f->_set_cooldown( $repo, $karr ), 1, 'level0 => 1m';
ok $f->_cooldown_active( $repo ), 'active right after set';
is $f->_set_cooldown( $repo, $karr ), 2, 'level1 => 2m';
is $f->_set_cooldown( $repo, $karr ), 4, 'level2 => 4m';
is $f->_set_cooldown( $repo, $karr ), 4, 'level3 capped at 4m';
$f->_clear_cooldown( $repo );
is $f->_state_get( $repo, 'cooldown_level' ), 0, 'cleared level back to 0';
$f->_state_set( $repo, cooldown_until => time - 1 );
ok ! $f->_cooldown_active( $repo ), 'past timestamp => inactive';
};
# ---------------------------------------------------------------------------
# Unit: auto-block via BoardStore
# ---------------------------------------------------------------------------
subtest '_autoblock_task' => sub {
my $repo = make_git_repo();
seed_board( $repo, { status => 'in-progress', claimed_by => 'a' } );
my $f = App::karr::Foundation->new;
ok $f->_autoblock_task( $repo, 1, 'auto: nope' ), 'autoblock returns true';
my $t = task_by_id( $repo, 1 );
ok $t->has_blocked, 'task is blocked';
is $t->blocked, 'auto: nope', 'block reason stored';
};
# ---------------------------------------------------------------------------
# Integration: drain to completion on progress
# ---------------------------------------------------------------------------
subtest 'drain completes when agent makes progress' => sub {
my $repo = make_git_repo();
seed_board( $repo, { status => 'todo' }, { status => 'todo' }, { status => 'todo' } );
my $agent = write_fake_agent( $repo );
my $f = App::karr::Foundation->new;
( run in 0.423 second using v1.01-cache-2.11-cpan-bbe5e583499 )