@@ -8,9 +8,198 @@ use std::path::Path;
88use st2:: message;
99use st2:: reconcile:: { Session , TaskTarget } ;
1010use st2:: run:: Runner ;
11- use st2:: run:: { CrashLoop , surface_crash_loop, up_once_selected_specs} ;
11+ use st2:: run:: { CrashLoop , surface_crash_loop, up_once_selected , up_once_selected_specs} ;
1212use st2:: spec:: { AgentSpec , JobType , Task , TaskKind } ;
1313
14+ fn selected_catalog_agent ( identity : & str , workspace : & Path , render : & str ) -> String {
15+ format ! (
16+ r#"agent "{identity}" {{
17+ host "host"
18+ type "service"
19+ workspace "{}"
20+ pty "work" {{
21+ id "host.{identity}.work"
22+ command "true"
23+ }}
24+ render {{
25+ {render}
26+ }}
27+ }}
28+ "# ,
29+ workspace. display( )
30+ )
31+ }
32+
33+ fn write_selected_catalog (
34+ catalog : & Path ,
35+ owner_workspace : & Path ,
36+ sibling_workspace : & Path ,
37+ owner_render : & str ,
38+ ) {
39+ fs:: create_dir_all ( owner_workspace) . unwrap ( ) ;
40+ fs:: create_dir_all ( sibling_workspace) . unwrap ( ) ;
41+ write (
42+ catalog,
43+ "agents/host/owner/agent.kdl" ,
44+ & selected_catalog_agent ( "owner" , owner_workspace, owner_render) ,
45+ ) ;
46+ write (
47+ catalog,
48+ "agents/host/sibling/agent.kdl" ,
49+ & selected_catalog_agent (
50+ "sibling" ,
51+ sibling_workspace,
52+ r#"file "SIBLING.txt" "sibling""# ,
53+ ) ,
54+ ) ;
55+ }
56+
57+ #[ test]
58+ fn selected_catalog_two_agent_kdl_recording_runner_matrix ( ) {
59+ enum Actual {
60+ Missing ,
61+ Live ,
62+ Dead ,
63+ }
64+
65+ for actual in [ Actual :: Missing , Actual :: Live , Actual :: Dead ] {
66+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
67+ let catalog = tmp. path ( ) . join ( "catalog" ) ;
68+ let owner_workspace = tmp. path ( ) . join ( "owner-workspace" ) ;
69+ let sibling_workspace = tmp. path ( ) . join ( "sibling-workspace" ) ;
70+ write_selected_catalog (
71+ & catalog,
72+ & owner_workspace,
73+ & sibling_workspace,
74+ r#"file "OWNER.txt" "owner""# ,
75+ ) ;
76+
77+ let mut sessions = vec ! [ live( "host.sibling.work" ) ] ;
78+ match actual {
79+ Actual :: Missing => { }
80+ Actual :: Live => sessions. push ( live ( "host.owner.work" ) ) ,
81+ Actual :: Dead => sessions. push ( dead ( "host.owner.work" ) ) ,
82+ }
83+ let runner = FakeRunner {
84+ sessions,
85+ ..Default :: default ( )
86+ } ;
87+
88+ let report = up_once_selected ( & catalog, "host.owner.work" , "host" , & runner) . unwrap ( ) ;
89+
90+ assert_eq ! ( runner. list_calls. get( ) , 1 ) ;
91+ assert_eq ! (
92+ fs:: read_to_string( owner_workspace. join( "OWNER.txt" ) ) . unwrap( ) ,
93+ "owner"
94+ ) ;
95+ assert ! (
96+ !sibling_workspace. join( "SIBLING.txt" ) . exists( ) ,
97+ "the unrelated owner must not be materialized"
98+ ) ;
99+ assert ! ( runner. killed. borrow( ) . is_empty( ) ) ;
100+ assert ! ( runner. removed. borrow( ) . is_empty( ) ) ;
101+ match actual {
102+ Actual :: Missing => {
103+ assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
104+ assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
105+ assert_eq ! ( report. launched, [ "host.owner.work" ] ) ;
106+ }
107+ Actual :: Live => {
108+ assert ! ( runner. spawned. borrow( ) . is_empty( ) ) ;
109+ assert ! ( runner. reaped. borrow( ) . is_empty( ) ) ;
110+ assert_eq ! ( report. adopted, [ "owner" ] ) ;
111+ }
112+ Actual :: Dead => {
113+ assert_eq ! ( runner. reaped. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
114+ assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
115+ assert_eq ! ( report. gc, [ "host.owner.work" ] ) ;
116+ assert_eq ! ( report. launched, [ "host.owner.work" ] ) ;
117+ }
118+ }
119+ assert ! (
120+ runner
121+ . spawned
122+ . borrow( )
123+ . iter( )
124+ . chain( runner. reaped. borrow( ) . iter( ) )
125+ . all( |id| id == "host.owner.work" )
126+ ) ;
127+ }
128+ }
129+
130+ #[ test]
131+ fn selected_catalog_surfaces_unrelated_malformed_diagnostics_without_blocking_owner ( ) {
132+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
133+ let catalog = tmp. path ( ) . join ( "catalog" ) ;
134+ let owner_workspace = tmp. path ( ) . join ( "owner-workspace" ) ;
135+ let sibling_workspace = tmp. path ( ) . join ( "sibling-workspace" ) ;
136+ write_selected_catalog (
137+ & catalog,
138+ & owner_workspace,
139+ & sibling_workspace,
140+ r#"file "OWNER.txt" "owner""# ,
141+ ) ;
142+ write (
143+ & catalog,
144+ "agents/host/sibling/broken.kdl" ,
145+ r#"agent "broken" {"# ,
146+ ) ;
147+ let runner = FakeRunner {
148+ sessions : vec ! [ live( "host.sibling.work" ) ] ,
149+ ..Default :: default ( )
150+ } ;
151+
152+ let report = up_once_selected ( & catalog, "host.owner.work" , "host" , & runner) . unwrap ( ) ;
153+
154+ assert_eq ! ( runner. spawned. borrow( ) . as_slice( ) , [ "host.owner.work" ] ) ;
155+ assert_eq ! (
156+ fs:: read_to_string( owner_workspace. join( "OWNER.txt" ) ) . unwrap( ) ,
157+ "owner"
158+ ) ;
159+ assert ! ( !sibling_workspace. join( "SIBLING.txt" ) . exists( ) ) ;
160+ assert ! (
161+ report
162+ . errors
163+ . iter( )
164+ . any( |error| error. contains( "broken.kdl" ) && error. contains( "KDL" ) ) ,
165+ "{:?}" ,
166+ report. errors
167+ ) ;
168+ }
169+
170+ #[ test]
171+ fn selected_catalog_owner_render_failure_refuses_runner_actions ( ) {
172+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
173+ let catalog = tmp. path ( ) . join ( "catalog" ) ;
174+ let owner_workspace = tmp. path ( ) . join ( "owner-workspace" ) ;
175+ let sibling_workspace = tmp. path ( ) . join ( "sibling-workspace" ) ;
176+ write_selected_catalog (
177+ & catalog,
178+ & owner_workspace,
179+ & sibling_workspace,
180+ r#"copy "_templates/missing" "OWNER.txt""# ,
181+ ) ;
182+ let runner = FakeRunner {
183+ sessions : vec ! [ live( "host.sibling.work" ) ] ,
184+ ..Default :: default ( )
185+ } ;
186+
187+ let report = up_once_selected ( & catalog, "host.owner.work" , "host" , & runner) . unwrap ( ) ;
188+
189+ assert_eq ! ( runner. list_calls. get( ) , 0 ) ;
190+ assert_refusal ( & runner) ;
191+ assert ! ( !owner_workspace. join( "OWNER.txt" ) . exists( ) ) ;
192+ assert ! ( !sibling_workspace. join( "SIBLING.txt" ) . exists( ) ) ;
193+ assert ! (
194+ report
195+ . errors
196+ . iter( )
197+ . any( |error| error. contains( "_templates/missing" ) ) ,
198+ "{:?}" ,
199+ report. errors
200+ ) ;
201+ }
202+
14203#[ test]
15204fn selected_one_shot_unknown_refuses_before_runner_list ( ) {
16205 let runner = FakeRunner :: default ( ) ;
0 commit comments