use super::*;
use crate::core::content_source::test_support::rc;
fn write_settings(dir: &Path, json: &str) {
let claude_dir = dir.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(claude_dir.join("settings.json"), json).unwrap();
}
fn write_local_settings(dir: &Path, json: &str) {
let claude_dir = dir.join(".claude");
std::fs::create_dir_all(&claude_dir).unwrap();
std::fs::write(claude_dir.join("settings.local.json"), json).unwrap();
}
fn deploy_default_style(home: &Path) {
let styles_dir = home.join(".claude").join("output-styles");
std::fs::create_dir_all(&styles_dir).unwrap();
let default = OUTPUT_STYLES[0];
std::fs::write(
styles_dir.join(default.file_name),
default.content(crate::core::content_source::test_support::rc()),
)
.unwrap();
}
fn hermetic_paths(home: &Path) -> FrameworkPaths {
let mut paths = FrameworkPaths::under(home);
paths.trusty_mpm_root = None;
paths
}
fn deploy_all_styles(home: &Path) {
let styles_dir = home.join(".claude").join("output-styles");
std::fs::create_dir_all(&styles_dir).unwrap();
for style in OUTPUT_STYLES {
std::fs::write(styles_dir.join(style.file_name), style.content(rc())).unwrap();
}
}
#[test]
fn output_style_ok_when_style_resolves() {
let home = tempfile::tempdir().unwrap();
deploy_default_style(home.path());
write_settings(home.path(), r#"{"outputStyle": "trusty-mpm"}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn output_style_fail_when_id_unknown() {
let home = tempfile::tempdir().unwrap();
write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("claude_mpm"));
assert!(
check.message.contains("trusty-mpm"),
"valid id list must be present"
);
}
#[test]
fn output_style_warn_when_key_absent() {
let home = tempfile::tempdir().unwrap();
write_settings(home.path(), r#"{}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn output_style_warn_when_settings_missing() {
let home = tempfile::tempdir().unwrap();
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Warn);
}
#[test]
fn output_style_fail_when_file_missing() {
let home = tempfile::tempdir().unwrap();
write_settings(home.path(), r#"{"outputStyle": "trusty-mpm"}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Fail);
}
#[test]
fn output_style_fail_on_malformed_json() {
let home = tempfile::tempdir().unwrap();
write_settings(home.path(), "{not valid json");
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("not valid JSON"));
}
#[test]
fn output_style_prefers_project_over_global() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
deploy_default_style(home.path());
write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn output_style_falls_back_to_global_when_project_silent() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
write_settings(project.path(), r#"{"theme": "dark"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("claude_mpm"));
assert!(
check.message.contains("user"),
"message must name the resolved scope: {}",
check.message
);
}
#[test]
fn output_style_falls_back_to_global_when_project_missing() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Fail);
assert!(check.message.contains("claude_mpm"));
}
#[test]
fn output_style_reports_project_scope_on_success() {
let home = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
deploy_default_style(home.path());
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok);
assert!(
check.message.contains("project"),
"message must name the resolved scope: {}",
check.message
);
}
#[test]
fn output_style_project_local_wins_even_when_project_settings_correct() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
deploy_default_style(home.path());
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#); write_local_settings(project.path(), r#"{"outputStyle": "claude_mpm"}"#); let check = check_output_style(Some(project.path()), home.path());
assert_eq!(
check.status,
CheckStatus::Fail,
"message: {}",
check.message
);
assert!(check.message.contains("claude_mpm"));
assert!(
check.message.contains("project-local"),
"message must name the resolved scope: {}",
check.message
);
}
#[test]
fn output_style_project_local_overrides_project() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
deploy_all_styles(home.path());
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#);
write_local_settings(project.path(), r#"{"outputStyle": "trusty-mpm-teacher"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
assert!(check.message.contains("trusty-mpm-teacher"));
assert!(check.message.contains("project-local"));
}
#[test]
fn output_style_user_local_overrides_user() {
let home = crate::test_support::hermetic_temp_dir();
deploy_all_styles(home.path());
write_settings(home.path(), r#"{"outputStyle": "trusty-mpm"}"#);
write_local_settings(home.path(), r#"{"outputStyle": "trusty-mpm-teacher"}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
assert!(check.message.contains("trusty-mpm-teacher"));
assert!(check.message.contains("user-local"));
}
#[test]
fn output_style_project_scope_still_outranks_user_local() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
deploy_default_style(home.path());
write_local_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#); write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#); let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
assert!(check.message.contains("project"));
}
fn write_project_style(project: &Path, with_composite: bool) -> PathBuf {
let styles = project.join(crate::core::output_style::PROJECT_STYLES_DIR);
std::fs::create_dir_all(&styles).unwrap();
let style = styles.join("fixture-voice.md");
std::fs::write(&style, "---\nname: fixture-voice\n---\nProject voice.\n").unwrap();
if with_composite {
std::fs::write(styles.join("fixture-voice.tm-floor.md"), "composite").unwrap();
}
style
}
#[test]
fn output_style_ok_for_a_project_style_and_its_composite() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
let style = write_project_style(project.path(), true);
for id in ["fixture-voice", "fixture-voice.tm-floor"] {
write_local_settings(project.path(), &format!(r#"{{"outputStyle": "{id}"}}"#));
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok, "{id}: {}", check.message);
assert!(
check.message.contains(&style.display().to_string()),
"{id}: the project style path is named: {}",
check.message
);
assert!(check.message.contains("project-local"), "{}", check.message);
}
write_local_settings(project.path(), r#"{"outputStyle": "nope.tm-floor"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Fail, "{}", check.message);
assert!(check.message.contains("is not a known trusty-mpm style"));
}
#[test]
fn output_style_project_style_without_composite_is_reported() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
write_project_style(project.path(), false);
write_settings(
project.path(),
r#"{"outputStyle": "fixture-voice.tm-floor"}"#,
);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Fail, "{}", check.message);
assert!(
check.message.contains("fixture-voice.tm-floor.md"),
"{}",
check.message
);
write_settings(project.path(), r#"{"outputStyle": "fixture-voice"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
write_settings(home.path(), r#"{"outputStyle": "fixture-voice"}"#);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Fail, "{}", check.message);
}
#[test]
fn output_style_warn_names_all_checked_layers_when_none_configured() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
write_settings(project.path(), r#"{"theme": "dark"}"#);
let check = check_output_style(Some(project.path()), home.path());
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("settings.local.json"));
assert!(check.message.contains("settings.json"));
}
#[test]
fn staleness_ok_when_in_sync() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn doctor_recognises_the_supervisor_style() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
let supervisor = crate::core::session_profile::SUPERVISOR_OUTPUT_STYLE_ID;
assert!(
home.path()
.join(".claude/output-styles")
.join(format!("{supervisor}.md"))
.is_file()
);
let stale = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(stale.status, CheckStatus::Ok, "message: {}", stale.message);
write_settings(
home.path(),
&format!(r#"{{"outputStyle": "{supervisor}"}}"#),
);
let check = check_output_style(None, home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn staleness_warns_on_drift() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
let styles_dir = home.path().join(".claude").join("output-styles");
let first = &OUTPUT_STYLES[0];
std::fs::write(
styles_dir.join(first.file_name),
"stale pre-#2328 identity text",
)
.unwrap();
let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains(first.file_name));
assert!(
check
.message
.contains(crate::core::doctor_repair::OUTPUT_STYLE_REMEDY),
"the remedy must name the command that redeploys: {}",
check.message
);
assert!(
!check.message.contains("tm install"),
"#5866: `tm install` has no output-style step — naming it sends the \
operator to a no-op: {}",
check.message
);
}
#[test]
fn staleness_warns_on_orphan() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
let styles_dir = home.path().join(".claude").join("output-styles");
std::fs::write(styles_dir.join("claude-mpm.md"), "pre-rebrand lineage").unwrap();
let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("claude-mpm.md"));
assert!(
styles_dir.join("claude-mpm.md").exists(),
"orphan detection must never delete the foreign file"
);
}
#[test]
fn staleness_ok_when_dir_missing() {
let home = tempfile::tempdir().unwrap();
let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn staleness_ok_when_file_never_deployed() {
let home = tempfile::tempdir().unwrap();
deploy_default_style(home.path()); let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn staleness_orphan_exempts_configured_custom_id() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
write_settings(home.path(), r#"{"outputStyle": "my-custom-style"}"#);
let styles_dir = home.path().join(".claude").join("output-styles");
std::fs::write(
styles_dir.join("my-custom-style.md"),
"operator's own style",
)
.unwrap();
let check = check_output_style_staleness(None, home.path(), &hermetic_paths(home.path()));
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn staleness_warns_on_managed_config_drift() {
let home = tempfile::tempdir().unwrap();
deploy_all_styles(home.path());
let paths = hermetic_paths(home.path());
let managed_styles = paths.managed_claude_config_dir().join("output-styles");
std::fs::create_dir_all(&managed_styles).unwrap();
let first = &OUTPUT_STYLES[0];
for style in OUTPUT_STYLES {
std::fs::write(managed_styles.join(style.file_name), style.content(rc())).unwrap();
}
std::fs::write(
managed_styles.join(first.file_name),
"stale pre-1.5.29 managed copy",
)
.unwrap();
let check = check_output_style_staleness(None, home.path(), &paths);
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(
check.message.contains("managed config"),
"the finding must name the tier: {}",
check.message
);
assert!(
check.message.contains(first.file_name),
"the finding must name the file: {}",
check.message
);
assert!(
!check.message.contains("operator home"),
"the operator's own copy is in sync and must not be reported: {}",
check.message
);
}
#[test]
fn legacy_ids_warns_on_shadowed_layer() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
deploy_default_style(home.path());
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#); write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
let effective = check_output_style(Some(project.path()), home.path());
assert_eq!(
effective.status,
CheckStatus::Ok,
"sanity: effective layer must resolve fine, message: {}",
effective.message
);
let check = check_output_style_legacy_ids(Some(project.path()), home.path());
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
assert!(check.message.contains("claude_mpm"));
assert!(
check.message.contains(
&home
.path()
.join(".claude")
.join("settings.json")
.display()
.to_string()
),
"message must name the exact offending file: {}",
check.message
);
}
#[test]
fn legacy_ids_does_not_double_report_the_effective_layer() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
write_local_settings(project.path(), r#"{"outputStyle": "claude_mpm"}"#); write_settings(home.path(), r#"{"outputStyle": "claude_mpm"}"#);
let check = check_output_style_legacy_ids(Some(project.path()), home.path());
assert_eq!(
check.status,
CheckStatus::Warn,
"message: {}",
check.message
);
let user_path = home.path().join(".claude").join("settings.json");
let project_local_path = project.path().join(".claude").join("settings.local.json");
assert!(
check.message.contains(&user_path.display().to_string()),
"must name the shadowed offender: {}",
check.message
);
assert!(
!check
.message
.contains(&project_local_path.display().to_string()),
"must NOT double-report the effective layer (already `Fail`ed by \
check_output_style): {}",
check.message
);
}
#[test]
fn legacy_ids_ok_when_all_known() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
deploy_all_styles(home.path());
write_settings(project.path(), r#"{"outputStyle": "trusty-mpm"}"#); write_settings(home.path(), r#"{"outputStyle": "trusty-mpm-teacher"}"#);
let check = check_output_style_legacy_ids(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}
#[test]
fn legacy_ids_ok_when_none_configured() {
let home = crate::test_support::hermetic_temp_dir();
let project = crate::test_support::hermetic_temp_dir();
let check = check_output_style_legacy_ids(Some(project.path()), home.path());
assert_eq!(check.status, CheckStatus::Ok, "message: {}", check.message);
}