Claude-Agent

 view release on metacpan or  search on metacpan

t/05-hooks.t  view on Meta::CPAN

);

isa_ok($matcher, 'Claude::Agent::Hook::Matcher');
is($matcher->timeout, 30, 'timeout set correctly');

# Test matches() - exact match
ok($matcher->matches('Bash'), 'matches exact tool name');
ok(!$matcher->matches('Read'), 'does not match different tool');
ok(!$matcher->matches('BashOutput'), 'exact match does not match partial');

# Test matches() - regex pattern
my $regex_matcher = Claude::Agent::Hook::Matcher->new(
    matcher => 'mcp__.*',
    hooks   => [],
);

ok($regex_matcher->matches('mcp__math__calculate'), 'regex matches mcp tool');
ok($regex_matcher->matches('mcp__server__tool'), 'regex matches another mcp tool');
ok(!$regex_matcher->matches('Read'), 'regex does not match non-mcp tool');

# Test matches() - no matcher (match all)
my $catch_all = Claude::Agent::Hook::Matcher->new(
    hooks => [],
);

ok($catch_all->matches('Bash'), 'no matcher matches Bash');
ok($catch_all->matches('Read'), 'no matcher matches Read');
ok($catch_all->matches('anything'), 'no matcher matches anything');

# Test run_hooks()
my @call_log;
my $logging_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub {
            my ($input, $tool_use_id, $context) = @_;
            push @call_log, { hook => 1, input => $input };
            return { decision => 'continue' };
        },
        sub {
            my ($input, $tool_use_id, $context) = @_;
            push @call_log, { hook => 2, input => $input };
            return { decision => 'continue' };
        },
    ],
);

my $input_data = { tool_name => 'Test', tool_input => { arg => 'value' } };
# run_hooks now returns a Future
my $future = $logging_matcher->run_hooks($input_data, 'tool-id-123', {});
isa_ok($future, 'Future', 'run_hooks returns a Future');
my $results = $future->get;

is(scalar @$results, 2, 'both hooks ran');
is(scalar @call_log, 2, 'both hooks logged');
is($call_log[0]{hook}, 1, 'first hook ran first');
is($call_log[1]{hook}, 2, 'second hook ran second');

# Test early termination on deny
my $deny_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub { return { decision => 'deny', reason => 'Blocked' } },
        sub { return { decision => 'continue' } },  # Should not run
    ],
);

my @deny_log;
$deny_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub {
            push @deny_log, 1;
            return { decision => 'deny', reason => 'Blocked' };
        },
        sub {
            push @deny_log, 2;
            return { decision => 'continue' };
        },
    ],
);

$future = $deny_matcher->run_hooks({}, 'id', {});
$results = $future->get;
is(scalar @deny_log, 1, 'second hook did not run after deny');
is($results->[0]{decision}, 'deny', 'deny result returned');

# Test error handling in hooks
my $error_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub { die "Hook error!" },
    ],
);

$future = $error_matcher->run_hooks({}, 'id', {});
$results = $future->get;
is($results->[0]{decision}, 'error', 'error caught and reported');
is($results->[0]{error}, 'Hook execution failed', 'error message sanitized');

# Test async hooks (returning Future)
use Future;
my $async_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub {
            my ($input, $tool_use_id, $context, $loop) = @_;
            # Return an immediate Future
            return Future->done({ decision => 'allow', reason => 'Async allowed' });
        },
    ],
);

$future = $async_matcher->run_hooks({}, 'id', {});
isa_ok($future, 'Future', 'async hook returns Future');
$results = $future->get;
is($results->[0]{decision}, 'allow', 'async hook decision returned');
is($results->[0]{reason}, 'Async allowed', 'async hook reason returned');

# Test async hook with failure
my $async_fail_matcher = Claude::Agent::Hook::Matcher->new(
    hooks => [
        sub {
            return Future->fail("Async hook failed");
        },
    ],
);

$future = $async_fail_matcher->run_hooks({}, 'id', {});
$results = $future->get;
is($results->[0]{decision}, 'error', 'async failure caught');
is($results->[0]{error}, 'Hook execution failed', 'async error message sanitized');

# Test Hook::Result factory methods
my $continue_result = Claude::Agent::Hook::Result->proceed();
is($continue_result->{decision}, 'continue', 'Result::proceed()');



( run in 0.637 second using v1.01-cache-2.11-cpan-5a3173703d6 )