@@ -256,6 +256,40 @@ pub fn uninstall_skills(dry_run: bool, _verbose: bool) -> Result<SkillsInstallRe
256256#[ cfg( test) ]
257257mod tests {
258258 use super :: * ;
259+ use serial_test:: serial;
260+
261+ /// Temporarily override HOME (and USERPROFILE on Windows) to a temp directory
262+ /// for the duration of the closure. Must only be called from `#[serial]` tests
263+ /// to avoid racing with other tests that read HOME.
264+ fn with_temp_home < F : FnOnce ( & std:: path:: Path ) > ( f : F ) {
265+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
266+ let home = temp. path ( ) . to_path_buf ( ) ;
267+
268+ let prev_home = std:: env:: var_os ( "HOME" ) ;
269+ let prev_userprofile = std:: env:: var_os ( "USERPROFILE" ) ;
270+
271+ // SAFETY: tests using this helper are serialized via #[serial],
272+ // so mutating the process environment is safe.
273+ unsafe {
274+ std:: env:: set_var ( "HOME" , & home) ;
275+ std:: env:: set_var ( "USERPROFILE" , & home) ;
276+ }
277+
278+ f ( & home) ;
279+
280+ // SAFETY: tests using this helper are serialized via #[serial],
281+ // so restoring the process environment is safe.
282+ unsafe {
283+ match prev_home {
284+ Some ( v) => std:: env:: set_var ( "HOME" , v) ,
285+ None => std:: env:: remove_var ( "HOME" ) ,
286+ }
287+ match prev_userprofile {
288+ Some ( v) => std:: env:: set_var ( "USERPROFILE" , v) ,
289+ None => std:: env:: remove_var ( "USERPROFILE" ) ,
290+ }
291+ }
292+ }
259293
260294 #[ test]
261295 fn test_embedded_skills_are_loaded ( ) {
@@ -399,46 +433,51 @@ mod tests {
399433 }
400434
401435 #[ test]
436+ #[ serial]
402437 fn test_install_and_uninstall_skills_lifecycle ( ) {
403- let skills_base = skills_dir_path ( ) . unwrap ( ) ;
404-
405- // Dry run should not create anything
406- let dry_result = install_skills ( true , false ) . unwrap ( ) ;
407- assert ! ( dry_result. changed) ;
408- assert_eq ! ( dry_result. installed_count, EMBEDDED_SKILLS . len( ) ) ;
409-
410- // Install creates skill files with correct content
411- let result = install_skills ( false , false ) . unwrap ( ) ;
412- assert ! ( result. changed) ;
413- assert_eq ! ( result. installed_count, EMBEDDED_SKILLS . len( ) ) ;
414- assert ! ( skills_base. exists( ) ) ;
415- for skill in EMBEDDED_SKILLS {
416- let skill_md = skills_base. join ( skill. name ) . join ( "SKILL.md" ) ;
417- assert ! ( skill_md. exists( ) , "SKILL.md missing for {}" , skill. name) ;
418- let content = fs:: read_to_string ( & skill_md) . unwrap ( ) ;
419- assert_eq ! ( content, skill. skill_md) ;
420- }
438+ // Use an isolated temp HOME so we don't pollute the real home directory
439+ // and don't race with other tests that mutate HOME (e.g. codex tests).
440+ with_temp_home ( |_home| {
441+ let skills_base = skills_dir_path ( ) . unwrap ( ) ;
442+
443+ // Dry run should not create anything
444+ let dry_result = install_skills ( true , false ) . unwrap ( ) ;
445+ assert ! ( dry_result. changed) ;
446+ assert_eq ! ( dry_result. installed_count, EMBEDDED_SKILLS . len( ) ) ;
447+
448+ // Install creates skill files with correct content
449+ let result = install_skills ( false , false ) . unwrap ( ) ;
450+ assert ! ( result. changed) ;
451+ assert_eq ! ( result. installed_count, EMBEDDED_SKILLS . len( ) ) ;
452+ assert ! ( skills_base. exists( ) ) ;
453+ for skill in EMBEDDED_SKILLS {
454+ let skill_md = skills_base. join ( skill. name ) . join ( "SKILL.md" ) ;
455+ assert ! ( skill_md. exists( ) , "SKILL.md missing for {}" , skill. name) ;
456+ let content = fs:: read_to_string ( & skill_md) . unwrap ( ) ;
457+ assert_eq ! ( content, skill. skill_md) ;
458+ }
421459
422- // Install again is idempotent
423- let result2 = install_skills ( false , false ) . unwrap ( ) ;
424- assert ! ( result2. changed) ;
425- for skill in EMBEDDED_SKILLS {
426- let skill_md = skills_base. join ( skill. name ) . join ( "SKILL.md" ) ;
427- assert ! (
428- skill_md. exists( ) ,
429- "SKILL.md missing after re-install for {}" ,
430- skill. name
431- ) ;
432- }
460+ // Install again is idempotent
461+ let result2 = install_skills ( false , false ) . unwrap ( ) ;
462+ assert ! ( result2. changed) ;
463+ for skill in EMBEDDED_SKILLS {
464+ let skill_md = skills_base. join ( skill. name ) . join ( "SKILL.md" ) ;
465+ assert ! (
466+ skill_md. exists( ) ,
467+ "SKILL.md missing after re-install for {}" ,
468+ skill. name
469+ ) ;
470+ }
433471
434- // Uninstall removes skills directory
435- let uninstall_result = uninstall_skills ( false , false ) . unwrap ( ) ;
436- assert ! ( uninstall_result. changed) ;
437- assert ! ( !skills_base. exists( ) ) ;
472+ // Uninstall removes skills directory
473+ let uninstall_result = uninstall_skills ( false , false ) . unwrap ( ) ;
474+ assert ! ( uninstall_result. changed) ;
475+ assert ! ( !skills_base. exists( ) ) ;
438476
439- // Uninstall again is a no-op
440- let noop_result = uninstall_skills ( false , false ) . unwrap ( ) ;
441- assert ! ( !noop_result. changed) ;
442- assert_eq ! ( noop_result. installed_count, 0 ) ;
477+ // Uninstall again is a no-op
478+ let noop_result = uninstall_skills ( false , false ) . unwrap ( ) ;
479+ assert ! ( !noop_result. changed) ;
480+ assert_eq ! ( noop_result. installed_count, 0 ) ;
481+ } ) ;
443482 }
444483}
0 commit comments