SNAPKITTYWEST commited on
Commit
9c54f86
·
verified ·
1 Parent(s): 894656d

Add test/hk_agent_fsm_tests.erl

Browse files
Files changed (1) hide show
  1. test/hk_agent_fsm_tests.erl +84 -0
test/hk_agent_fsm_tests.erl ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ %%%-------------------------------------------------------------------
2
+ %%% @doc Covers: deterministic state transitions, authorization
3
+ %%% boundaries, and loop-limit enforcement (the "DAG-like agent
4
+ %%% workflows", "authorization boundaries", and "deterministic state
5
+ %%% transitions" items from `<test>').
6
+ %%% @end
7
+ %%%-------------------------------------------------------------------
8
+ -module(hk_agent_fsm_tests).
9
+ -include_lib("eunit/include/eunit.hrl").
10
+
11
+ setup() -> hk_test_helper:start_app().
12
+ teardown(Ok) -> hk_test_helper:stop_app(Ok).
13
+
14
+ agent_fixture_test_() ->
15
+ {setup, fun setup/0, fun teardown/1, fun(_) -> [
16
+ {"created -> ready -> planning -> ... deterministic transitions",
17
+ fun deterministic_transitions/0},
18
+ {"unauthorized tool call is rejected without reaching the browser",
19
+ fun unauthorized_call_rejected/0},
20
+ {"invalid arguments are rejected at the control-plane boundary",
21
+ fun invalid_arguments_rejected/0},
22
+ {"exceeding max_actions transitions the agent to failed",
23
+ fun limit_exceeded_fails_agent/0},
24
+ {"agent process crash does not affect an unrelated agent",
25
+ fun crash_isolation/0}
26
+ ] end}.
27
+
28
+ deterministic_transitions() ->
29
+ AgentId = hk_id:new(<<"agent">>),
30
+ {ok, Pid} = hk_agent_fsm_sup:start_agent(#{agent_id => AgentId, capabilities => [read_page]}),
31
+ {ok, #{state := created}} = as_map(hk_agent_fsm:describe(Pid)),
32
+ ok = hk_agent_fsm:assign_objective(Pid, <<"test objective">>),
33
+ {ok, #{state := ready}} = as_map(hk_agent_fsm:describe(Pid)),
34
+ %% A tool call the agent lacks any browser session for still drives
35
+ %% the state machine through planning -> executing -> waiting ->
36
+ %% planning; it fails at the toolbox level (no such session), which
37
+ %% is a normal, observed action outcome, not a stuck state.
38
+ {error, _} = hk_agent_fsm:next_action(Pid, browser, read_page,
39
+ #{session_id => <<"missing">>, tab_id => <<"missing">>}),
40
+ {ok, #{state := planning, action_count := 1}} = as_map(hk_agent_fsm:describe(Pid)),
41
+ ok = hk_agent_fsm:complete(Pid, #{summary => <<"done">>}),
42
+ {ok, #{state := completed}} = as_map(hk_agent_fsm:describe(Pid)).
43
+
44
+ unauthorized_call_rejected() ->
45
+ AgentId = hk_id:new(<<"agent">>),
46
+ {ok, Pid} = hk_agent_fsm_sup:start_agent(#{agent_id => AgentId, capabilities => [read_page]}),
47
+ ok = hk_agent_fsm:assign_objective(Pid, <<"obj">>),
48
+ Result = hk_agent_fsm:next_action(Pid, browser, click,
49
+ #{session_id => <<"s">>, tab_id => <<"t">>, selector => <<"#x">>}),
50
+ ?assertEqual({error, not_authorized}, Result),
51
+ %% Action count must not increase for a call that never reached the toolbox.
52
+ {ok, #{action_count := 0}} = as_map(hk_agent_fsm:describe(Pid)).
53
+
54
+ invalid_arguments_rejected() ->
55
+ AgentId = hk_id:new(<<"agent">>),
56
+ {ok, Pid} = hk_agent_fsm_sup:start_agent(#{agent_id => AgentId, capabilities => [navigate]}),
57
+ ok = hk_agent_fsm:assign_objective(Pid, <<"obj">>),
58
+ Result = hk_agent_fsm:next_action(Pid, browser, navigate,
59
+ #{session_id => <<"s">>, tab_id => <<"t">>,
60
+ url => <<"javascript:evil()">>}),
61
+ ?assertMatch({error, {invalid_arguments, _}}, Result).
62
+
63
+ limit_exceeded_fails_agent() ->
64
+ AgentId = hk_id:new(<<"agent">>),
65
+ {ok, Pid} = hk_agent_fsm_sup:start_agent(#{agent_id => AgentId, capabilities => [read_page],
66
+ limits => #{max_actions => 0}}),
67
+ ok = hk_agent_fsm:assign_objective(Pid, <<"obj">>),
68
+ Result = hk_agent_fsm:next_action(Pid, browser, read_page, #{session_id => <<"s">>, tab_id => <<"t">>}),
69
+ ?assertMatch({error, {limit_exceeded, max_actions}}, Result),
70
+ {ok, #{state := failed, failure_reason := Reason}} = as_map(hk_agent_fsm:describe(Pid)),
71
+ ?assertMatch({limit_exceeded, max_actions}, Reason).
72
+
73
+ crash_isolation() ->
74
+ {ok, PidA} = hk_agent_fsm_sup:start_agent(#{agent_id => hk_id:new(<<"agent">>), capabilities => []}),
75
+ {ok, PidB} = hk_agent_fsm_sup:start_agent(#{agent_id => hk_id:new(<<"agent">>), capabilities => []}),
76
+ Ref = monitor(process, PidA),
77
+ exit(PidA, kill),
78
+ receive {'DOWN', Ref, process, PidA, killed} -> ok after 2000 -> ?assert(false) end,
79
+ timer:sleep(100),
80
+ ?assert(is_process_alive(PidB)),
81
+ ?assert(is_process_alive(whereis(hk_agent_sup))),
82
+ ?assert(is_process_alive(whereis(hyperkitty_sup))).
83
+
84
+ as_map({ok, Record}) -> {ok, hk_schema:to_map(Record)}.