use super::*;
use std::collections::BTreeSet;
fn stacks(stems: &[&str]) -> BTreeSet<String> {
stems.iter().map(|s| (*s).to_string()).collect()
}
fn detection(stems: &[&str]) -> StackDetection {
StackDetection {
engineers: stacks(stems),
truncated: false,
depth_limited: false,
}
}
fn settings_path(project: &Path) -> PathBuf {
project.join(".claude").join("settings.json")
}
fn read_overrides(project: &Path) -> serde_json::Value {
let text = std::fs::read_to_string(settings_path(project)).expect("settings.json exists");
let value: serde_json::Value = serde_json::from_str(&text).expect("valid JSON");
value[SKILL_OVERRIDES_KEY].clone()
}
fn assert_table_applied(project_stacks: &[&str]) {
let tmp = tempfile::tempdir().unwrap();
let detected = stacks(project_stacks);
write_skill_overrides_for(tmp.path(), &detection(project_stacks)).expect("write succeeds");
let overrides = read_overrides(tmp.path());
let mut saw_relevant = false;
let mut saw_irrelevant = false;
for family in SKILL_FAMILY_TABLE {
let relevant = family.relevant_stacks.iter().any(|s| detected.contains(*s));
for skill in family.skills {
if relevant {
saw_relevant = true;
assert!(
overrides.get(*skill).is_none(),
"{project_stacks:?}: family `{}` is relevant, but `{skill}` was turned off",
family.name
);
} else {
saw_irrelevant = true;
assert_eq!(
overrides[*skill],
serde_json::json!(OFF),
"{project_stacks:?}: family `{}` is irrelevant, but `{skill}` is still listed",
family.name
);
}
}
}
assert!(
saw_relevant && saw_irrelevant,
"{project_stacks:?} must exercise both directions of the table"
);
}
#[test]
fn rust_project_turns_off_every_family_the_table_marks_irrelevant() {
assert_table_applied(&["rust-engineer"]);
}
#[test]
fn svelte_project_turns_off_every_family_the_table_marks_irrelevant() {
assert_table_applied(&[
"javascript-engineer",
"svelte-engineer",
"typescript-engineer",
]);
}
#[test]
fn rust_and_svelte_disagree_on_the_rust_and_web_families() {
let rust = irrelevant_skills(&stacks(&["rust-engineer"]));
let svelte = irrelevant_skills(&stacks(&["javascript-engineer", "svelte-engineer"]));
assert!(!rust.contains("rust-build-performance") && svelte.contains("rust-build-performance"));
assert!(!rust.contains("rust-delivery-workflow") && svelte.contains("rust-delivery-workflow"));
assert!(rust.contains("webapp-testing") && !svelte.contains("webapp-testing"));
}
#[test]
fn polyglot_project_keeps_a_family_any_of_its_stacks_uses() {
let off = irrelevant_skills(&stacks(&["rust-engineer", "javascript-engineer"]));
assert!(!off.contains("rust-build-performance"));
assert!(!off.contains("webapp-testing"));
assert!(off.contains("breeze-voice"));
}
#[test]
fn every_table_stack_is_a_detectable_stem() {
let categories = crate::core::manifest::framework::framework_agent_categories()
.expect("bundled framework manifest is valid");
let detectable: BTreeSet<&str> = categories
.language
.iter()
.chain(categories.framework.iter())
.map(|entry| entry.stem.as_str())
.collect();
for family in SKILL_FAMILY_TABLE {
for stem in family.relevant_stacks {
assert!(
detectable.contains(stem),
"family `{}` names `{stem}`, which the detector never returns",
family.name
);
}
}
}
#[test]
fn unknown_stack_turns_nothing_off() {
let tmp = tempfile::tempdir().unwrap();
assert!(irrelevant_skills(&BTreeSet::new()).is_empty());
let outcome = write_skill_overrides_for(tmp.path(), &detection(&[])).unwrap();
assert_eq!(outcome, SkillOverridesOutcome::NoStack);
assert!(!settings_path(tmp.path()).exists());
}
#[test]
fn unknown_stack_leaves_an_existing_file_byte_identical() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join(".claude")).unwrap();
let original = "{\"permissions\": {\"allow\": []}}\n";
std::fs::write(settings_path(tmp.path()), original).unwrap();
write_skill_overrides_for(tmp.path(), &detection(&[])).unwrap();
assert_eq!(
std::fs::read_to_string(settings_path(tmp.path())).unwrap(),
original
);
}
#[test]
fn truncated_detection_writes_no_overrides() {
let truncated = StackDetection {
engineers: stacks(&["rust-engineer"]),
truncated: true,
depth_limited: false,
};
assert!(!irrelevant_skills(&truncated.engineers).is_empty());
let fresh = tempfile::tempdir().unwrap();
assert_eq!(
write_skill_overrides_for(fresh.path(), &truncated).unwrap(),
SkillOverridesOutcome::Truncated
);
assert!(
!settings_path(fresh.path()).exists(),
"a truncated detection must not create the settings file"
);
let existing = tempfile::tempdir().unwrap();
std::fs::create_dir_all(existing.path().join(".claude")).unwrap();
let original = "{\"permissions\": {\"allow\": []}}\n";
std::fs::write(settings_path(existing.path()), original).unwrap();
assert_eq!(
write_skill_overrides_for(existing.path(), &truncated).unwrap(),
SkillOverridesOutcome::Truncated
);
assert_eq!(
std::fs::read_to_string(settings_path(existing.path())).unwrap(),
original,
"a truncated detection must leave an existing file byte-for-byte"
);
}
#[test]
fn depth_limited_detection_still_writes() {
let tmp = tempfile::tempdir().unwrap();
let deep = StackDetection {
engineers: stacks(&["rust-engineer"]),
truncated: false,
depth_limited: true,
};
let outcome = write_skill_overrides_for(tmp.path(), &deep).unwrap();
assert!(
matches!(outcome, SkillOverridesOutcome::Written(_)),
"{outcome:?}"
);
assert_eq!(
read_overrides(tmp.path())["breeze-voice"],
serde_json::json!(OFF)
);
}
#[test]
fn user_entries_and_foreign_keys_survive_the_merge() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join(".claude")).unwrap();
std::fs::write(
settings_path(tmp.path()),
r#"{"permissions":{"allow":["Bash(ls)"]},"skillOverrides":{"xlsx":"on","my-skill":"off"}}"#,
)
.unwrap();
write_skill_overrides_for(tmp.path(), &detection(&["rust-engineer"])).unwrap();
let text = std::fs::read_to_string(settings_path(tmp.path())).unwrap();
let value: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(
value["permissions"]["allow"],
serde_json::json!(["Bash(ls)"])
);
assert_eq!(
value[SKILL_OVERRIDES_KEY]["xlsx"],
serde_json::json!("on"),
"user entry wins"
);
assert_eq!(
value[SKILL_OVERRIDES_KEY]["my-skill"],
serde_json::json!("off")
);
assert_eq!(
value[SKILL_OVERRIDES_KEY]["breeze-voice"],
serde_json::json!(OFF)
);
}
#[test]
fn malformed_settings_are_left_untouched() {
for original in ["{ not json", "[1, 2]", r#"{"skillOverrides": "off"}"#] {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join(".claude")).unwrap();
std::fs::write(settings_path(tmp.path()), original).unwrap();
let outcome =
write_skill_overrides_for(tmp.path(), &detection(&["rust-engineer"])).unwrap();
assert!(
matches!(outcome, SkillOverridesOutcome::Skipped(_)),
"{original}: {outcome:?}"
);
assert_eq!(
std::fs::read_to_string(settings_path(tmp.path())).unwrap(),
original,
"a settings file tm cannot parse must survive byte-for-byte"
);
}
}
#[test]
fn second_write_is_byte_identical() {
let tmp = tempfile::tempdir().unwrap();
let rust = detection(&["rust-engineer"]);
assert!(matches!(
write_skill_overrides_for(tmp.path(), &rust).unwrap(),
SkillOverridesOutcome::Written(_)
));
let first = std::fs::read(settings_path(tmp.path())).unwrap();
assert_eq!(
write_skill_overrides_for(tmp.path(), &rust).unwrap(),
SkillOverridesOutcome::Unchanged
);
assert_eq!(std::fs::read(settings_path(tmp.path())).unwrap(), first);
}
#[test]
fn every_table_skill_is_bundled_or_vouched_for() {
let roster: BTreeSet<String> = crate::core::manifest::framework::framework_skill_categories()
.expect("bundled framework manifest is valid")
.universal
.into_iter()
.collect();
let vouched: BTreeSet<&str> = UNBUNDLED_TABLE_SKILLS.iter().copied().collect();
for family in SKILL_FAMILY_TABLE {
for skill in family.skills {
assert!(
roster.contains(*skill) || vouched.contains(*skill),
"family `{}` names `{skill}`, which is neither a bundled skill nor listed in \
UNBUNDLED_TABLE_SKILLS — a name no inventory knows turns nothing off",
family.name
);
}
}
let named: BTreeSet<&str> = SKILL_FAMILY_TABLE
.iter()
.flat_map(|family| family.skills.iter().copied())
.collect();
for skill in UNBUNDLED_TABLE_SKILLS {
assert!(
named.contains(*skill),
"`{skill}` is vouched for in UNBUNDLED_TABLE_SKILLS but no family names it"
);
}
}
#[test]
fn no_skill_appears_in_two_families() {
let mut seen: BTreeSet<&str> = BTreeSet::new();
for family in SKILL_FAMILY_TABLE {
for skill in family.skills {
assert!(
seen.insert(*skill),
"`{skill}` is listed by more than one family (`{}` is the second) — \
the flatten in `irrelevant_skills` would turn it off for both",
family.name
);
}
}
}
const RACE_TRIALS: usize = 40;
#[test]
fn a_concurrent_statusline_write_loses_no_skill_overrides() {
let rust = detection(&["rust-engineer"]);
let planned = irrelevant_skills(&rust.engineers);
let mut lost: Vec<String> = Vec::new();
for trial in 0..RACE_TRIALS {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path();
std::fs::create_dir_all(project.join(".claude")).unwrap();
let path = settings_path(project);
let start = std::sync::Barrier::new(2);
std::thread::scope(|scope| {
scope.spawn(|| {
start.wait();
write_skill_overrides_for(project, &rust).expect("the merge must never fail");
});
scope.spawn(|| {
start.wait();
crate::core::statusline_settings::ensure_statusline_entry_in(&path);
});
});
let text = std::fs::read_to_string(&path).expect("both writers create the file");
let value: serde_json::Value =
serde_json::from_str(&text).expect("a published settings.json is always valid JSON");
let mut missing: Vec<String> = Vec::new();
if value.get("statusLine").is_none() {
missing.push("statusLine".to_string());
}
for skill in &planned {
if value[SKILL_OVERRIDES_KEY].get(*skill).is_none() {
missing.push((*skill).to_string());
}
}
if !missing.is_empty() {
lost.push(format!("trial {trial} lost {missing:?}"));
}
}
assert!(
lost.is_empty(),
"{} of {RACE_TRIALS} concurrent trials lost an update — the read → mutate → write \
cycle stored a snapshot taken before a sibling writer's store (#7751, #4072). {lost:?}",
lost.len()
);
}