@@ -115,37 +115,164 @@ fn setup_script_warns_about_missing_audio_helper() {
115115 ) ;
116116}
117117
118- /// Verify the Jetson restart helper script is syntactically valid .
118+ /// Verify LLM backend auto-fallback can patch a root-owned config and fails loudly .
119119#[ test]
120- fn jetson_restart_script_is_valid_shell ( ) {
121- let path = workspace_root ( ) . join ( "deploy/scripts/genie-restart-all .sh" ) ;
122- assert ! ( path. exists ( ) , "restart helper script should exist" ) ;
120+ fn setup_script_privileged_llm_backend_patch_is_checked ( ) {
121+ let path = workspace_root ( ) . join ( "deploy/setup-jetson .sh" ) ;
122+ let contents = std :: fs :: read_to_string ( & path) . unwrap ( ) ;
123123
124- let output = std:: process:: Command :: new ( "bash" )
125- . args ( [ "-n" , path. to_str ( ) . unwrap ( ) ] )
126- . output ( )
127- . expect ( "failed to run bash -n" ) ;
124+ assert ! (
125+ contents. contains( "CONFIGURED_BACKEND=\" $(sudo awk" ) ,
126+ "setup script should read the configured LLM backend through sudo"
127+ ) ;
128+ assert ! (
129+ contents. contains( "if ! sudo awk -v nb=\" $new_backend\" -v nu=\" $new_unit\" " ) ,
130+ "setup script should read the chmod 600 root-owned config through sudo"
131+ ) ;
132+ assert ! (
133+ contents. contains( "sudo mktemp /tmp/geniepod.toml." ) ,
134+ "setup script should create a root-owned temp file for the patched config"
135+ ) ;
136+ assert ! (
137+ contents. contains( "ERROR: failed to rewrite $cfg for patching" ) ,
138+ "setup script should report failed config rewrites"
139+ ) ;
140+ assert ! (
141+ contents. contains( "| sudo tee \" $tmp\" > /dev/null" ) ,
142+ "setup script should write the patched temp file through sudo tee"
143+ ) ;
144+ assert ! (
145+ contents. contains( "ERROR: failed to install patched $cfg" ) ,
146+ "setup script should report failed config installs"
147+ ) ;
148+ assert ! (
149+ contents. contains( "sudo rm -f \" $tmp\" " ) ,
150+ "setup script should clean up the root-owned temp file through sudo"
151+ ) ;
152+ assert ! (
153+ contents. contains( "Installing genie-ai-runtime now; this is the default backend" ) ,
154+ "setup script should install the default runtime during normal setup"
155+ ) ;
156+ assert ! (
157+ contents. contains( "Downloading prebuilt runtime assets" ) ,
158+ "setup script should download the default runtime from release assets"
159+ ) ;
160+ assert ! (
161+ contents. contains( "SHA256SUMS" ) ,
162+ "setup script should download release checksums"
163+ ) ;
164+ assert ! (
165+ contents. contains( "sha256sum -c" ) ,
166+ "setup script should verify downloaded runtime checksums"
167+ ) ;
168+ assert ! (
169+ contents. contains( "jetson-llm-server-v1.0.0-aarch64-unknown-linux-gnu" ) ,
170+ "setup script should document the required server release asset"
171+ ) ;
172+ assert ! (
173+ !contents. contains( "git clone --branch \" $tag\" " ) ,
174+ "setup script should not clone the runtime repo during normal install"
175+ ) ;
176+ assert ! (
177+ !contents. contains( "cmake --build build" ) ,
178+ "setup script should not build the runtime from source during setup"
179+ ) ;
180+ assert ! (
181+ !contents. contains( "Auto-falling back to llama.cpp" ) ,
182+ "setup script should not silently downgrade the default backend to llama.cpp"
183+ ) ;
184+ assert ! (
185+ contents. contains(
186+ "if ! patch_services_llm_backend \" genie_ai_runtime\" \" genie-ai-runtime.service\" "
187+ ) ,
188+ "genie-ai-runtime selection should check patch failure"
189+ ) ;
190+ assert ! (
191+ contents
192+ . contains( "auto-fallback could not patch $CONFIG_DIR/geniepod.toml; aborting setup" ) ,
193+ "setup should abort instead of enabling services against an unpatched config"
194+ ) ;
195+ }
196+
197+ /// Verify the Jetson lifecycle helper scripts are syntactically valid.
198+ #[ test]
199+ fn jetson_lifecycle_scripts_are_valid_shell ( ) {
200+ for script in [
201+ "deploy/scripts/genie-restart-all.sh" ,
202+ "deploy/scripts/start_all.sh" ,
203+ "deploy/scripts/stop_all.sh" ,
204+ ] {
205+ let path = workspace_root ( ) . join ( script) ;
206+ assert ! ( path. exists( ) , "{script} should exist" ) ;
207+
208+ let output = std:: process:: Command :: new ( "bash" )
209+ . args ( [ "-n" , path. to_str ( ) . unwrap ( ) ] )
210+ . output ( )
211+ . expect ( "failed to run bash -n" ) ;
212+
213+ assert ! (
214+ output. status. success( ) ,
215+ "{script} has invalid shell syntax: {}" ,
216+ String :: from_utf8_lossy( & output. stderr)
217+ ) ;
218+ }
219+ }
220+
221+ /// Verify the deploy pipeline copies the Jetson lifecycle helper scripts.
222+ #[ test]
223+ fn makefile_deploys_lifecycle_helpers ( ) {
224+ let path = workspace_root ( ) . join ( "Makefile" ) ;
225+ let contents = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
226+
227+ for script in [ "genie-restart-all.sh" , "start_all.sh" , "stop_all.sh" ] {
228+ assert ! (
229+ contents. contains( & format!( "deploy/scripts/{script}" ) ) ,
230+ "Makefile should copy {script} during deploy"
231+ ) ;
232+ assert ! (
233+ contents. contains( & format!( "$(INSTALL_DIR)/bin/{script}" ) ) ,
234+ "Makefile should install {script} into /opt/geniepod/bin"
235+ ) ;
236+ }
237+ }
238+
239+ /// Verify start_all follows the configured backend instead of starting both LLMs.
240+ #[ test]
241+ fn start_all_uses_configured_llm_backend ( ) {
242+ let path = workspace_root ( ) . join ( "deploy/scripts/start_all.sh" ) ;
243+ let contents = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
128244
129245 assert ! (
130- output. status. success( ) ,
131- "restart helper script has invalid shell syntax: {}" ,
132- String :: from_utf8_lossy( & output. stderr)
246+ contents. contains( "Configured LLM unit" ) ,
247+ "start_all should report the selected LLM unit"
248+ ) ;
249+ assert ! (
250+ contents. contains( "read_llm_unit" ) ,
251+ "start_all should read [services.llm].systemd_unit"
252+ ) ;
253+ assert ! (
254+ contents. contains( "other_llm_units_for" ) ,
255+ "start_all should stop the non-selected LLM backend before starting"
256+ ) ;
257+ assert ! (
258+ contents. contains( "is_warmup_unit" ) && contents. contains( "start --no-block" ) ,
259+ "start_all should queue warmup units without blocking the lifecycle script"
133260 ) ;
134261}
135262
136- /// Verify the deploy pipeline copies the Jetson restart helper script .
263+ /// Verify systemd deploy replaces stale or masked unit-file symlinks .
137264#[ test]
138- fn makefile_deploys_restart_helper ( ) {
265+ fn makefile_installs_systemd_units_instead_of_copying_through_symlinks ( ) {
139266 let path = workspace_root ( ) . join ( "Makefile" ) ;
140267 let contents = std:: fs:: read_to_string ( & path) . unwrap ( ) ;
141268
142269 assert ! (
143- contents. contains( "deploy/scripts/genie-restart-all.sh " ) ,
144- "Makefile should copy the restart helper script during deploy "
270+ contents. contains( "sudo install -m 0644 \" $$unit \" " ) ,
271+ "Makefile should replace stale/masked unit files instead of copying through symlinks "
145272 ) ;
146273 assert ! (
147- contents. contains( "$(INSTALL_DIR)/bin /genie-restart-all.sh " ) ,
148- "Makefile should install the restart helper into /opt/geniepod/bin "
274+ ! contents. contains( "sudo cp /tmp /genie-*.service " ) ,
275+ "Makefile should not use cp for systemd units; cp follows masked-unit symlinks "
149276 ) ;
150277}
151278
0 commit comments