From 6845fb723cd0a27816cc6aeb5833034e238acc14 Mon Sep 17 00:00:00 2001 From: heAdz0r Date: Sat, 14 Feb 2026 22:56:34 +0300 Subject: [PATCH] refactor(init): add upsert_rtk_block for idempotent CLAUDE.md management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace naive append-or-skip logic in run_claude_md_mode with upsert_rtk_block() that handles all 4 cases: - Added: no existing block → append - Updated: stale block → replace in-place - Unchanged: current block → no-op - Malformed: opening marker without closing → warn safely Includes 4 unit tests covering each case. --- src/init.rs | 159 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/utils.rs | 1 - 2 files changed, 152 insertions(+), 8 deletions(-) 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("...")); } - }