use lifeloop::host_assets::{
self as ha, AssetStatus, FileAction, HostAdapter, IntegrationMode, MergedFile, RenderedAsset,
aggregate_status, asset_status, byte_equal_asset_status, ccd_compat_claude_command,
codex_hooks_contain_managed_lifecycle, combine_actions, merge_claude_settings,
merge_claude_settings_text, merge_codex_config_text, merge_codex_hooks, merge_codex_hooks_text,
render_applied_assets, render_required_source_assets, render_source_assets, supported_modes,
supports_mode,
};
use serde_json::{Value, json};
#[test]
fn host_adapter_round_trips_canonical_ids() {
for host in HostAdapter::ALL {
assert_eq!(HostAdapter::from_id(host.as_str()), Some(*host));
}
assert_eq!(
HostAdapter::from_id("claude-code"),
Some(HostAdapter::Claude)
);
assert_eq!(HostAdapter::from_id("unknown"), None);
}
#[test]
fn integration_mode_round_trips_wire_strings() {
for mode in IntegrationMode::ALL {
assert_eq!(IntegrationMode::from_id(mode.as_str()), Some(*mode));
}
assert_eq!(IntegrationMode::from_id("telemetry_only"), None);
}
#[test]
fn integration_mode_converts_from_lifecycle_mode() {
use lifeloop::IntegrationMode as Lifecycle;
assert_eq!(
IntegrationMode::from_lifecycle_mode(Lifecycle::NativeHook),
Some(IntegrationMode::NativeHook)
);
assert_eq!(
IntegrationMode::from_lifecycle_mode(Lifecycle::TelemetryOnly),
None,
"telemetry_only has no asset surface"
);
}
#[test]
fn supports_mode_pins_per_host_matrix() {
assert!(supports_mode(
HostAdapter::Claude,
IntegrationMode::NativeHook
));
assert!(!supports_mode(
HostAdapter::Claude,
IntegrationMode::ManualSkill
));
assert!(supports_mode(
HostAdapter::Codex,
IntegrationMode::ManualSkill
));
assert!(supports_mode(
HostAdapter::Codex,
IntegrationMode::LauncherWrapper
));
assert!(supports_mode(
HostAdapter::Codex,
IntegrationMode::NativeHook
));
assert!(!supports_mode(
HostAdapter::Codex,
IntegrationMode::ReferenceAdapter
));
assert!(supports_mode(
HostAdapter::Hermes,
IntegrationMode::ReferenceAdapter
));
assert!(supports_mode(
HostAdapter::OpenClaw,
IntegrationMode::ReferenceAdapter
));
assert!(!supports_mode(
HostAdapter::Hermes,
IntegrationMode::NativeHook
));
}
#[test]
fn supported_modes_lists_match_supports_mode() {
assert_eq!(
supported_modes(HostAdapter::Claude),
&[IntegrationMode::NativeHook]
);
assert_eq!(
supported_modes(HostAdapter::Codex),
&[
IntegrationMode::ManualSkill,
IntegrationMode::LauncherWrapper,
IntegrationMode::NativeHook,
]
);
assert_eq!(
supported_modes(HostAdapter::Hermes),
&[IntegrationMode::ReferenceAdapter]
);
assert_eq!(
supported_modes(HostAdapter::OpenClaw),
&[IntegrationMode::ReferenceAdapter]
);
for host in HostAdapter::ALL {
for mode in supported_modes(*host) {
assert!(
supports_mode(*host, *mode),
"{} should support {}",
host.as_str(),
mode.as_str()
);
}
}
}
#[test]
fn render_source_assets_for_each_host_pins_path_set() {
let claude = render_source_assets(HostAdapter::Claude);
assert_eq!(
claude.iter().map(|a| a.relative_path).collect::<Vec<_>>(),
vec![ha::CLAUDE_SOURCE_SETTINGS]
);
let codex = render_source_assets(HostAdapter::Codex);
let codex_paths: Vec<_> = codex.iter().map(|a| a.relative_path).collect();
assert_eq!(
codex_paths,
vec![
ha::CODEX_SOURCE_README,
ha::CODEX_SOURCE_CONFIG,
ha::CODEX_SOURCE_HOOKS,
ha::CODEX_SOURCE_LAUNCHER,
]
);
let launcher = codex
.iter()
.find(|a| a.relative_path == ha::CODEX_SOURCE_LAUNCHER)
.expect("launcher present");
assert_eq!(launcher.mode, Some(0o755), "launcher must be executable");
assert_eq!(
render_source_assets(HostAdapter::Hermes)
.iter()
.map(|a| a.relative_path)
.collect::<Vec<_>>(),
vec![ha::HERMES_SOURCE_ADAPTER]
);
assert_eq!(
render_source_assets(HostAdapter::OpenClaw)
.iter()
.map(|a| a.relative_path)
.collect::<Vec<_>>(),
vec![ha::OPENCLAW_SOURCE_ADAPTER]
);
}
#[test]
fn render_required_source_assets_filters_codex_modes() {
let manual = render_required_source_assets(HostAdapter::Codex, IntegrationMode::ManualSkill);
assert_eq!(
manual.iter().map(|a| a.relative_path).collect::<Vec<_>>(),
vec![ha::CODEX_SOURCE_README]
);
let launcher =
render_required_source_assets(HostAdapter::Codex, IntegrationMode::LauncherWrapper);
let paths: Vec<_> = launcher.iter().map(|a| a.relative_path).collect();
assert_eq!(
paths,
vec![ha::CODEX_SOURCE_README, ha::CODEX_SOURCE_LAUNCHER]
);
let native = render_required_source_assets(HostAdapter::Codex, IntegrationMode::NativeHook);
let native_paths: Vec<_> = native.iter().map(|a| a.relative_path).collect();
assert_eq!(
native_paths,
vec![
ha::CODEX_SOURCE_README,
ha::CODEX_SOURCE_CONFIG,
ha::CODEX_SOURCE_HOOKS,
]
);
}
#[test]
fn render_applied_assets_per_host_mode_matches_install_matrix() {
let claude = render_applied_assets(HostAdapter::Claude, IntegrationMode::NativeHook);
assert_eq!(
claude.iter().map(|a| a.relative_path).collect::<Vec<_>>(),
vec![ha::CLAUDE_TARGET_SETTINGS]
);
let codex_native = render_applied_assets(HostAdapter::Codex, IntegrationMode::NativeHook);
let native_paths: Vec<_> = codex_native.iter().map(|a| a.relative_path).collect();
assert_eq!(
native_paths,
vec![ha::CODEX_TARGET_CONFIG, ha::CODEX_TARGET_HOOKS]
);
let codex_launcher =
render_applied_assets(HostAdapter::Codex, IntegrationMode::LauncherWrapper);
let launcher_assets: Vec<_> = codex_launcher.iter().collect();
assert_eq!(launcher_assets.len(), 1);
assert_eq!(launcher_assets[0].relative_path, ha::CODEX_TARGET_LAUNCHER);
assert_eq!(launcher_assets[0].mode, Some(0o755));
let codex_manual = render_applied_assets(HostAdapter::Codex, IntegrationMode::ManualSkill);
assert!(codex_manual.is_empty());
assert_eq!(
render_applied_assets(HostAdapter::Hermes, IntegrationMode::ReferenceAdapter)
.iter()
.map(|a| a.relative_path)
.collect::<Vec<_>>(),
vec![ha::HERMES_TARGET_ADAPTER]
);
assert_eq!(
render_applied_assets(HostAdapter::OpenClaw, IntegrationMode::ReferenceAdapter)
.iter()
.map(|a| a.relative_path)
.collect::<Vec<_>>(),
vec![ha::OPENCLAW_TARGET_ADAPTER]
);
}
#[test]
fn rendered_claude_settings_carries_managed_command_prefix() {
let claude = render_applied_assets(HostAdapter::Claude, IntegrationMode::NativeHook);
let settings = &claude[0];
let parsed: Value = serde_json::from_str(&settings.contents).expect("valid json");
let session_start_cmd = parsed["hooks"]["SessionStart"][0]["hooks"][0]["command"]
.as_str()
.expect("session_start command");
assert!(
session_start_cmd.starts_with(ha::CCD_COMPAT_CLAUDE_COMMAND_PREFIX),
"command must start with CCD-compat Claude prefix: {session_start_cmd}"
);
assert_eq!(
session_start_cmd,
ccd_compat_claude_command("on-session-start")
);
}
#[test]
fn rendered_codex_hooks_contain_full_managed_lifecycle() {
let codex = render_applied_assets(HostAdapter::Codex, IntegrationMode::NativeHook);
let hooks_asset = codex
.iter()
.find(|a| a.relative_path == ha::CODEX_TARGET_HOOKS)
.expect("hooks.json present");
let parsed: Value = serde_json::from_str(&hooks_asset.contents).expect("valid json");
assert!(
codex_hooks_contain_managed_lifecycle(&parsed),
"rendered Codex hooks must satisfy codex_hooks_contain_managed_lifecycle"
);
}
#[test]
fn merge_claude_into_empty_object_produces_all_managed_events() {
let merged = merge_claude_settings(json!({})).expect("merge should succeed on empty");
let hooks = &merged["hooks"];
for event in [
"SessionStart",
"UserPromptSubmit",
"PreCompact",
"Stop",
"SessionEnd",
] {
assert!(hooks.get(event).is_some(), "missing event {event}");
}
assert!(
hooks.get("TaskCompleted").is_none(),
"TaskCompleted must not be installed"
);
}
#[test]
fn merge_claude_preserves_unrelated_top_level_keys() {
let input = json!({
"theme": "dark",
"hooks": {
"Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "echo user" }] }]
}
});
let merged = merge_claude_settings(input).expect("merge should succeed");
assert_eq!(merged["theme"], Value::String("dark".to_owned()));
let stop_hooks = merged["hooks"]["Stop"][0]["hooks"].as_array().unwrap();
assert_eq!(stop_hooks.len(), 2);
assert_eq!(
stop_hooks[0]["command"],
Value::String("echo user".to_owned())
);
assert!(
stop_hooks[1]["command"]
.as_str()
.unwrap()
.starts_with(ha::CCD_COMPAT_CLAUDE_COMMAND_PREFIX)
);
}
#[test]
fn merge_claude_is_idempotent_across_two_merges() {
let input = json!({ "theme": "dark" });
let once = merge_claude_settings(input).expect("first merge");
let twice = merge_claude_settings(once.clone()).expect("second merge");
assert_eq!(
serde_json::to_string_pretty(&once).unwrap(),
serde_json::to_string_pretty(&twice).unwrap(),
);
}
#[test]
fn merge_claude_replaces_pre_v1_python_entries() {
let input = json!({
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ccd-hook.py on-agent-end"
}]
}]
}
});
let merged = merge_claude_settings(input).expect("merge should succeed");
let stop_hooks = merged["hooks"]["Stop"][0]["hooks"].as_array().unwrap();
assert_eq!(stop_hooks.len(), 1);
assert!(
stop_hooks[0]["command"]
.as_str()
.unwrap()
.starts_with(ha::CCD_COMPAT_CLAUDE_COMMAND_PREFIX)
);
}
#[test]
fn merge_claude_removes_retired_task_completed_python_bridge_entries() {
let input = json!({
"hooks": {
"TaskCompleted": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ccd-hook.py on-agent-end"
}]
}],
"Stop": [{
"matcher": "*",
"hooks": []
}]
}
});
let merged = merge_claude_settings(input).expect("merge should succeed");
assert!(
merged["hooks"].get("TaskCompleted").is_none(),
"retired CCD TaskCompleted entry should be removed: {merged}"
);
}
#[test]
fn merge_claude_preserves_user_owned_task_completed_entries() {
let input = json!({
"hooks": {
"TaskCompleted": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "echo user-task-completed"
}]
}]
}
});
let merged = merge_claude_settings(input).expect("merge should succeed");
let hooks = merged["hooks"]["TaskCompleted"][0]["hooks"]
.as_array()
.unwrap();
assert_eq!(hooks[0]["command"], "echo user-task-completed");
}
#[test]
fn merge_claude_returns_none_when_hooks_is_not_an_object() {
let input = json!({ "hooks": [1, 2, 3] });
assert!(merge_claude_settings(input).is_none());
}
#[test]
fn merge_claude_returns_none_when_managed_event_is_not_an_array() {
let input = json!({ "hooks": { "Stop": { "unexpected": "shape" } } });
assert!(merge_claude_settings(input).is_none());
}
#[test]
fn merge_claude_text_force_rebuilds_when_input_is_malformed_top_level() {
let merged = merge_claude_settings_text(Some("[1, 2, 3]"), true)
.expect("ok")
.expect("force rebuild yields merged");
let parsed: Value = serde_json::from_str(&merged.rendered).expect("parseable");
assert!(
parsed["hooks"]["Stop"].is_array(),
"rebuild should populate managed events"
);
}
#[test]
fn merge_claude_text_force_rebuilds_when_json_is_invalid() {
let merged = merge_claude_settings_text(Some("{ not json"), true)
.expect("ok")
.expect("force rebuild yields merged");
let parsed: Value = serde_json::from_str(&merged.rendered).expect("parseable");
assert!(parsed["hooks"]["SessionStart"].is_array());
assert!(
merge_claude_settings_text(Some("{ not json"), false)
.expect("ok")
.is_none()
);
}
#[test]
fn merge_claude_text_force_rebuilds_when_managed_event_shape_is_bad() {
let malformed = r#"{"hooks":{"Stop":{"unexpected":"shape"}}}"#;
let merged = merge_claude_settings_text(Some(malformed), true)
.expect("ok")
.expect("force rebuild yields merged");
let parsed: Value = serde_json::from_str(&merged.rendered).expect("parseable");
assert!(parsed["hooks"]["Stop"].is_array());
assert!(
merge_claude_settings_text(Some(malformed), false)
.expect("ok")
.is_none()
);
}
#[test]
fn merge_claude_text_no_force_returns_none_when_file_is_malformed() {
assert!(
merge_claude_settings_text(Some("[1, 2, 3]"), false)
.expect("ok")
.is_none()
);
}
#[test]
fn merge_claude_text_creates_from_scratch_when_existing_is_none() {
let merged = merge_claude_settings_text(None, false)
.expect("ok")
.expect("creates new file");
assert!(merged.existing.is_none());
let parsed: Value = serde_json::from_str(&merged.rendered).expect("valid json");
assert!(parsed["hooks"]["SessionStart"].is_array());
}
#[test]
fn merge_codex_into_empty_object_produces_codex_lifecycle_hooks() {
let merged = merge_codex_hooks(json!({})).expect("merge");
assert!(codex_hooks_contain_managed_lifecycle(&merged));
}
#[test]
fn merge_codex_preserves_user_hooks_in_same_event() {
let input = json!({
"hooks": {
"Stop": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "echo user-owned-stop-hook" }]
}]
}
});
let merged = merge_codex_hooks(input).expect("merge");
let stop_hooks = merged["hooks"]["Stop"][0]["hooks"].as_array().unwrap();
assert_eq!(stop_hooks[0]["command"], "echo user-owned-stop-hook");
assert!(stop_hooks.iter().any(|hook| {
hook["command"]
.as_str()
.unwrap()
.starts_with(ha::CCD_COMPAT_CODEX_COMMAND_PREFIX)
}));
}
#[test]
fn merge_codex_is_idempotent() {
let once = merge_codex_hooks(json!({})).expect("first merge");
let twice = merge_codex_hooks(once.clone()).expect("second merge");
assert_eq!(once, twice);
}
#[test]
fn merge_codex_rejects_malformed_event_shape() {
let input = json!({ "hooks": { "Stop": { "unexpected": "shape" } } });
assert!(merge_codex_hooks(input).is_none());
}
#[test]
fn merge_codex_text_returns_none_for_invalid_json() {
assert!(
merge_codex_hooks_text(Some("not json"), false)
.expect("ok")
.is_none()
);
}
#[test]
fn merge_codex_text_creates_from_scratch_when_none() {
let merged = merge_codex_hooks_text(None, false)
.expect("ok")
.expect("creates new file");
let parsed: Value = serde_json::from_str(&merged.rendered).unwrap();
assert!(codex_hooks_contain_managed_lifecycle(&parsed));
}
#[test]
fn merge_codex_config_creates_features_table_when_absent() {
let merged = merge_codex_config_text(None).expect("ok");
assert!(
merged.rendered.contains("[features]"),
"rendered TOML should declare [features] table: {}",
merged.rendered
);
assert!(
merged.rendered.contains("hooks = true"),
"rendered TOML should set hooks=true: {}",
merged.rendered
);
}
#[test]
fn merge_codex_config_treats_whitespace_body_as_empty() {
let merged = merge_codex_config_text(Some(" \n\t \n")).expect("ok");
let parsed: toml::Value = merged.rendered.parse().expect("parseable");
assert_eq!(parsed["features"]["hooks"].as_bool(), Some(true));
}
#[test]
fn merge_codex_config_preserves_unrelated_keys() {
let existing = "[other]\nvalue = 42\n";
let merged = merge_codex_config_text(Some(existing)).expect("ok");
let parsed: toml::Value = merged.rendered.parse().expect("parseable");
assert_eq!(parsed["other"]["value"].as_integer(), Some(42));
assert_eq!(parsed["features"]["hooks"].as_bool(), Some(true));
}
#[test]
fn merge_codex_config_idempotent() {
let once = merge_codex_config_text(None).expect("once");
let twice = merge_codex_config_text(Some(&once.rendered)).expect("twice");
assert_eq!(once.rendered, twice.rendered);
}
#[test]
fn merge_codex_config_rejects_non_table_top_level() {
let err = merge_codex_config_text(Some("invalid = ["))
.expect_err("parse error expected for malformed TOML");
let _ = err.to_string();
}
#[test]
fn byte_equal_status_present_when_content_matches() {
let asset = RenderedAsset {
relative_path: ".something/template",
contents: "hello\n".into(),
mode: None,
};
assert_eq!(
byte_equal_asset_status(&asset, Some("hello\n"), None),
AssetStatus::Present
);
}
#[test]
fn byte_equal_status_drifted_on_content_difference() {
let asset = RenderedAsset {
relative_path: ".something/template",
contents: "hello\n".into(),
mode: None,
};
assert_eq!(
byte_equal_asset_status(&asset, Some("goodbye\n"), None),
AssetStatus::Drifted
);
}
#[test]
fn byte_equal_status_missing_when_file_absent() {
let asset = RenderedAsset {
relative_path: ".something/template",
contents: "hello\n".into(),
mode: None,
};
assert_eq!(
byte_equal_asset_status(&asset, None, None),
AssetStatus::Missing
);
}
#[test]
fn byte_equal_status_drift_on_mode_mismatch() {
let asset = RenderedAsset {
relative_path: ".something/launcher.sh",
contents: "exec\n".into(),
mode: Some(0o755),
};
assert_eq!(
byte_equal_asset_status(&asset, Some("exec\n"), Some(0o644)),
AssetStatus::Drifted
);
assert_eq!(
byte_equal_asset_status(&asset, Some("exec\n"), Some(0o755)),
AssetStatus::Present
);
}
#[test]
fn claude_settings_status_present_after_merge_round_trip() {
let merged = merge_claude_settings_text(None, false)
.expect("ok")
.expect("rendered");
assert_eq!(
ha::claude_settings_status(Some(&merged.rendered)),
AssetStatus::Present
);
}
#[test]
fn claude_settings_status_missing_when_no_file() {
assert_eq!(ha::claude_settings_status(None), AssetStatus::Missing);
}
#[test]
fn claude_settings_status_drifted_when_managed_entry_was_changed() {
let merged = merge_claude_settings_text(None, false)
.expect("ok")
.expect("rendered");
let mutated = merged.rendered.replacen("on-session-start", "tampered", 1);
assert_eq!(
ha::claude_settings_status(Some(&mutated)),
AssetStatus::Drifted
);
}
#[test]
fn codex_hooks_status_round_trip() {
let merged = merge_codex_hooks_text(None, false)
.expect("ok")
.expect("rendered");
assert_eq!(
ha::codex_hooks_status(Some(&merged.rendered)),
AssetStatus::Present
);
assert_eq!(ha::codex_hooks_status(None), AssetStatus::Missing);
let mutated = merged.rendered.replacen("on-session-start", "tampered", 1);
assert_eq!(ha::codex_hooks_status(Some(&mutated)), AssetStatus::Drifted);
}
#[test]
fn codex_config_status_present_only_when_feature_enabled() {
assert_eq!(
ha::codex_config_status(Some("[features]\nhooks = true\n")),
AssetStatus::Present
);
assert_eq!(
ha::codex_config_status(Some("[features]\nhooks = false\n")),
AssetStatus::Drifted
);
assert_eq!(
ha::codex_config_status(Some("[features]\ncodex_hooks = true\n")),
AssetStatus::Drifted
);
assert_eq!(
ha::codex_config_status(Some("not toml ===")),
AssetStatus::Drifted
);
assert_eq!(ha::codex_config_status(None), AssetStatus::Missing);
}
#[test]
fn asset_status_dispatches_to_per_path_strategy() {
let merged = merge_claude_settings_text(None, false)
.expect("ok")
.expect("rendered");
let asset = RenderedAsset {
relative_path: ha::CLAUDE_TARGET_SETTINGS,
contents: "not the comparison source".into(),
mode: None,
};
assert_eq!(
asset_status(&asset, Some(&merged.rendered), None),
AssetStatus::Present
);
let codex_config = RenderedAsset {
relative_path: ha::CODEX_TARGET_CONFIG,
contents: "not the comparison source".into(),
mode: None,
};
assert_eq!(
asset_status(&codex_config, Some("[features]\nhooks = true\n"), None),
AssetStatus::Present
);
let merged_hooks = merge_codex_hooks_text(None, false)
.expect("ok")
.expect("rendered");
let codex_hooks = RenderedAsset {
relative_path: ha::CODEX_TARGET_HOOKS,
contents: "not the comparison source".into(),
mode: None,
};
assert_eq!(
asset_status(&codex_hooks, Some(&merged_hooks.rendered), None),
AssetStatus::Present
);
let other = RenderedAsset {
relative_path: ".something/other",
contents: "abc".into(),
mode: None,
};
assert_eq!(
asset_status(&other, Some("abc"), None),
AssetStatus::Present
);
assert_eq!(
asset_status(&other, Some("xyz"), None),
AssetStatus::Drifted
);
}
#[test]
fn aggregate_status_pins_drift_dominates_missing_dominates_present() {
assert_eq!(
aggregate_status([AssetStatus::Present, AssetStatus::Present]),
AssetStatus::Present
);
assert_eq!(
aggregate_status([AssetStatus::Present, AssetStatus::Missing]),
AssetStatus::Missing
);
assert_eq!(
aggregate_status([AssetStatus::Missing, AssetStatus::Drifted]),
AssetStatus::Drifted
);
assert_eq!(
aggregate_status(std::iter::empty::<AssetStatus>()),
AssetStatus::NotApplicable
);
}
#[test]
fn combine_actions_pins_priority() {
assert_eq!(
combine_actions(FileAction::AlreadyPresent, FileAction::AlreadyPresent),
FileAction::AlreadyPresent
);
assert_eq!(
combine_actions(FileAction::AlreadyPresent, FileAction::Installed),
FileAction::Installed
);
assert_eq!(
combine_actions(FileAction::Installed, FileAction::AlreadyPresent),
FileAction::Installed
);
assert_eq!(
combine_actions(FileAction::Installed, FileAction::Updated),
FileAction::Updated
);
assert_eq!(
combine_actions(FileAction::Updated, FileAction::AlreadyPresent),
FileAction::Updated
);
}
#[test]
fn end_to_end_apply_claude_settings_writes_then_reports_present() {
let tmp = tempfile::tempdir().expect("tempdir");
let target = tmp.path().join(ha::CLAUDE_TARGET_SETTINGS);
std::fs::create_dir_all(target.parent().unwrap()).unwrap();
let existing = match std::fs::read_to_string(&target) {
Ok(s) => Some(s),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
Err(err) => panic!("read: {err}"),
};
let merged: MergedFile = merge_claude_settings_text(existing.as_deref(), false)
.expect("ok")
.expect("rendered");
std::fs::write(&target, &merged.rendered).unwrap();
let on_disk = std::fs::read_to_string(&target).unwrap();
assert_eq!(
ha::claude_settings_status(Some(&on_disk)),
AssetStatus::Present
);
let remerged = merge_claude_settings_text(Some(&on_disk), false)
.expect("ok")
.expect("rendered");
assert_eq!(remerged.rendered, on_disk);
}