diff --git a/src/init.rs b/src/init.rs index 482f9f8..961e4ac 100644 --- a/src/init.rs +++ b/src/init.rs @@ -767,15 +767,48 @@ fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> { if path.exists() { let existing = fs::read_to_string(&path)?; + // upsert_rtk_block handles all 4 cases: add, update, unchanged, malformed + let (new_content, action) = upsert_rtk_block(&existing, RTK_INSTRUCTIONS); - if existing.contains(""; + + if let Some(start) = content.find(start_marker) { + if let Some(relative_end) = content[start..].find(end_marker) { + let end = start + relative_end; + let end_pos = end + end_marker.len(); + let current_block = content[start..end_pos].trim(); + let desired_block = block.trim(); + + if current_block == desired_block { + return (content.to_string(), RtkBlockUpsert::Unchanged); + } + + // Replace stale block with desired block + let before = content[..start].trim_end(); + let after = content[end_pos..].trim_start(); + + let result = match (before.is_empty(), after.is_empty()) { + (true, true) => desired_block.to_string(), + (true, false) => format!("{desired_block}\n\n{after}"), + (false, true) => format!("{before}\n\n{desired_block}"), + (false, false) => format!("{before}\n\n{desired_block}\n\n{after}"), + }; + + return (result, RtkBlockUpsert::Updated); + } + + // Opening marker without closing marker — malformed + return (content.to_string(), RtkBlockUpsert::Malformed); + } + + // No existing block — append + let trimmed = content.trim(); + if trimmed.is_empty() { + (block.to_string(), RtkBlockUpsert::Added) + } else { + ( + format!("{trimmed}\n\n{}", block.trim()), + RtkBlockUpsert::Added, + ) + } +} + /// Patch CLAUDE.md: add @RTK.md, migrate if old block exists fn patch_claude_md(path: &Path, verbose: u8) -> Result { let mut content = if path.exists() { @@ -1103,6 +1199,55 @@ More content"#; assert!(RTK_INSTRUCTIONS.len() > 4000); } + // --- upsert_rtk_block tests --- + + #[test] + fn test_upsert_rtk_block_appends_when_missing() { + let input = "# Team instructions"; + let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS); + assert_eq!(action, RtkBlockUpsert::Added); + assert!(content.contains("# Team instructions")); + assert!(content.contains(" +OLD RTK CONTENT + + +More notes +"#; + + let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS); + assert_eq!(action, RtkBlockUpsert::Updated); + assert!(!content.contains("OLD RTK CONTENT")); + assert!(content.contains("rtk cargo test")); // from current RTK_INSTRUCTIONS + assert!(content.contains("# Team instructions")); + assert!(content.contains("More notes")); + } + + #[test] + fn test_upsert_rtk_block_noop_when_already_current() { + let input = format!( + "# Team instructions\n\n{}\n\nMore notes\n", + RTK_INSTRUCTIONS + ); + let (content, action) = upsert_rtk_block(&input, RTK_INSTRUCTIONS); + assert_eq!(action, RtkBlockUpsert::Unchanged); + assert_eq!(content, input); + } + + #[test] + fn test_upsert_rtk_block_detects_malformed_block() { + let input = "\npartial"; + let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS); + assert_eq!(action, RtkBlockUpsert::Malformed); + assert_eq!(content, input); + } + #[test] fn test_init_is_idempotent() { let temp = TempDir::new().unwrap(); diff --git a/src/utils.rs b/src/utils.rs index dbf9c91..6ea0698 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -395,5 +395,4 @@ mod tests { let result = truncate(cjk, 6); assert!(result.ends_with("...")); } - }