Skip to content

Commit 1dad61a

Browse files
jwiegleyclaude
andcommitted
Fix Ubuntu CI race condition in skills_installer test
The test_install_and_uninstall_skills_lifecycle test was failing on Ubuntu CI because codex tests temporarily replace HOME with a tempdir path. With 8 parallel test threads, the lifecycle test could call home_dir() while HOME pointed to a codex test's temp directory that gets dropped mid-execution, causing ENOENT errors. Add #[serial] to serialize the test with codex tests and wrap the test body in with_temp_home for full HOME isolation, matching the pattern already used by codex tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 92bcb46 commit 1dad61a

1 file changed

Lines changed: 76 additions & 37 deletions

File tree

src/mdm/skills_installer.rs

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,40 @@ pub fn uninstall_skills(dry_run: bool, _verbose: bool) -> Result<SkillsInstallRe
256256
#[cfg(test)]
257257
mod 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

Comments
 (0)