1use std::path::PathBuf;
2
3const MARKER: &str = "# lean-ctx — Context Engineering Layer";
4const END_MARKER: &str = "<!-- /lean-ctx -->";
5const RULES_VERSION: &str = "lean-ctx-rules-v5";
6
7const RULES_SHARED: &str = r#"# lean-ctx — Context Engineering Layer
13<!-- lean-ctx-rules-v5 -->
14
15MANDATORY: Use lean-ctx MCP tools for ALL reads and searches. Native Read/Grep/Shell are FORBIDDEN.
16
17| FORBIDDEN | USE INSTEAD |
18|-----------|-------------|
19| `Read` / `cat` / `head` / `tail` | `ctx_read(path)` — cached, 8 compression modes |
20| `Shell` / `bash` / terminal | `ctx_shell(command)` — pattern compression |
21| `Grep` / `rg` / search | `ctx_search(pattern, path)` — compact results |
22| `ls` / `find` / `tree` | `ctx_tree(path, depth)` — directory maps |
23
24Keep using Write, StrReplace, Delete, Glob normally (no lean-ctx replacement).
25
26REMINDER: NEVER use Read, Shell, Grep, or ls. ALWAYS use ctx_read, ctx_shell, ctx_search, ctx_tree.
27<!-- /lean-ctx -->"#;
28
29const RULES_DEDICATED: &str = r#"# lean-ctx — Context Engineering Layer
35<!-- lean-ctx-rules-v5 -->
36
37MANDATORY: Use lean-ctx MCP tools for ALL reads and searches. Native Read/Grep/Shell are FORBIDDEN.
38
39## Tool replacements (MUST use):
40| FORBIDDEN | USE INSTEAD | Why |
41|-----------|-------------|-----|
42| `Read` / `cat` / `head` | `ctx_read(path)` | Cached, 8 compression modes, re-reads ~13 tokens |
43| `Shell` / `bash` | `ctx_shell(command)` | Pattern compression for git/npm/cargo output |
44| `Grep` / `rg` | `ctx_search(pattern, path)` | Compact, token-efficient results |
45| `ls` / `find` | `ctx_tree(path, depth)` | Compact directory maps |
46
47## ctx_read modes:
48- `full` — cached read (files you edit)
49- `map` — deps + exports (context-only files)
50- `signatures` — API surface only
51- `diff` — changed lines after edits
52- `lines:N-M` — specific range
53
54## Keep using (no lean-ctx replacement):
55Write, StrReplace, Delete, Glob — use normally.
56
57## Proactive (use without being asked):
58- `ctx_overview(task)` at session start
59- `ctx_compress` when context grows large
60
61REMINDER: NEVER use Read, Shell, Grep, or ls. ALWAYS use ctx_read, ctx_shell, ctx_search, ctx_tree.
62<!-- /lean-ctx -->"#;
63
64const RULES_CURSOR_MDC: &str = r#"---
70description: "lean-ctx: ALWAYS use ctx_read/ctx_shell/ctx_search/ctx_tree instead of Read/Shell/Grep/ls"
71alwaysApply: true
72---
73
74# lean-ctx — Context Engineering Layer
75<!-- lean-ctx-rules-v5 -->
76
77CRITICAL: NEVER use native Read, Shell, Grep, or ls tools. ALWAYS use lean-ctx MCP equivalents below.
78
79## Tool Mapping (MANDATORY)
80
81| FORBIDDEN | USE INSTEAD | Why |
82|-----------|-------------|-----|
83| `Read` | `ctx_read` | Session caching, 8 compression modes, re-reads cost ~13 tokens |
84| `Shell` | `ctx_shell` | Pattern-based compression for git, npm, cargo, docker, tsc |
85| `Grep` | `ctx_search` | Compact context, token-efficient results |
86| `ls`, `find` | `ctx_tree` | Compact directory maps with file counts |
87
88## ctx_read Modes
89
90- `full` — default, cached read (use for files you will edit)
91- `map` — dependency graph + exports + key signatures (use for context-only files)
92- `signatures` — API surface only
93- `diff` — changed lines only (use after edits)
94- `lines:N-M` — specific range
95
96## Rules
97
98- Use `ctx_read(path, mode="full")` for every file read before editing
99- Use `ctx_read(path, mode="map")` for files you only need as context
100- Use `ctx_shell(cmd)` for every shell command
101- Use `ctx_search(pattern, path)` instead of Grep
102- Use `ctx_tree` instead of `ls` or `find` for directory exploration
103- The `Write`, `StrReplace`, `Delete`, `Glob` tools have no lean-ctx equivalent — use them normally
104
105REMINDER: NEVER use native Read, Shell, Grep, or ls. ALWAYS use ctx_read, ctx_shell, ctx_search, ctx_tree. Every single time.
106<!-- /lean-ctx -->"#;
107
108struct RulesTarget {
111 name: &'static str,
112 path: PathBuf,
113 format: RulesFormat,
114}
115
116enum RulesFormat {
117 SharedMarkdown,
118 DedicatedMarkdown,
119 CursorMdc,
120}
121
122pub struct InjectResult {
123 pub injected: Vec<String>,
124 pub updated: Vec<String>,
125 pub already: Vec<String>,
126 pub errors: Vec<String>,
127}
128
129pub fn inject_all_rules(home: &std::path::Path) -> InjectResult {
130 let targets = build_rules_targets(home);
131
132 let mut result = InjectResult {
133 injected: Vec::new(),
134 updated: Vec::new(),
135 already: Vec::new(),
136 errors: Vec::new(),
137 };
138
139 for target in &targets {
140 if !is_tool_detected(target, home) {
141 continue;
142 }
143
144 match inject_rules(target) {
145 Ok(RulesResult::Injected) => result.injected.push(target.name.to_string()),
146 Ok(RulesResult::Updated) => result.updated.push(target.name.to_string()),
147 Ok(RulesResult::AlreadyPresent) => result.already.push(target.name.to_string()),
148 Err(e) => result.errors.push(format!("{}: {e}", target.name)),
149 }
150 }
151
152 result
153}
154
155enum RulesResult {
160 Injected,
161 Updated,
162 AlreadyPresent,
163}
164
165fn rules_content(format: &RulesFormat) -> &'static str {
166 match format {
167 RulesFormat::SharedMarkdown => RULES_SHARED,
168 RulesFormat::DedicatedMarkdown => RULES_DEDICATED,
169 RulesFormat::CursorMdc => RULES_CURSOR_MDC,
170 }
171}
172
173fn inject_rules(target: &RulesTarget) -> Result<RulesResult, String> {
174 if target.path.exists() {
175 let content = std::fs::read_to_string(&target.path).map_err(|e| e.to_string())?;
176 if content.contains(MARKER) {
177 if content.contains(RULES_VERSION) {
178 return Ok(RulesResult::AlreadyPresent);
179 }
180 ensure_parent(&target.path)?;
181 return match target.format {
182 RulesFormat::SharedMarkdown => replace_markdown_section(&target.path, &content),
183 RulesFormat::DedicatedMarkdown | RulesFormat::CursorMdc => {
184 write_dedicated(&target.path, rules_content(&target.format))
185 }
186 };
187 }
188 }
189
190 ensure_parent(&target.path)?;
191
192 match target.format {
193 RulesFormat::SharedMarkdown => append_to_shared(&target.path),
194 RulesFormat::DedicatedMarkdown | RulesFormat::CursorMdc => {
195 write_dedicated(&target.path, rules_content(&target.format))
196 }
197 }
198}
199
200fn ensure_parent(path: &std::path::Path) -> Result<(), String> {
201 if let Some(parent) = path.parent() {
202 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
203 }
204 Ok(())
205}
206
207fn append_to_shared(path: &std::path::Path) -> Result<RulesResult, String> {
208 let mut content = if path.exists() {
209 std::fs::read_to_string(path).map_err(|e| e.to_string())?
210 } else {
211 String::new()
212 };
213
214 if !content.is_empty() && !content.ends_with('\n') {
215 content.push('\n');
216 }
217 if !content.is_empty() {
218 content.push('\n');
219 }
220 content.push_str(RULES_SHARED);
221 content.push('\n');
222
223 std::fs::write(path, content).map_err(|e| e.to_string())?;
224 Ok(RulesResult::Injected)
225}
226
227fn replace_markdown_section(path: &std::path::Path, content: &str) -> Result<RulesResult, String> {
228 let start = content.find(MARKER);
229 let end = content.find(END_MARKER);
230
231 let new_content = match (start, end) {
232 (Some(s), Some(e)) => {
233 let before = &content[..s];
234 let after_end = e + END_MARKER.len();
235 let after = content[after_end..].trim_start_matches('\n');
236 let mut result = before.to_string();
237 result.push_str(RULES_SHARED);
238 if !after.is_empty() {
239 result.push('\n');
240 result.push_str(after);
241 }
242 result
243 }
244 (Some(s), None) => {
245 let before = &content[..s];
246 let mut result = before.to_string();
247 result.push_str(RULES_SHARED);
248 result.push('\n');
249 result
250 }
251 _ => return Ok(RulesResult::AlreadyPresent),
252 };
253
254 std::fs::write(path, new_content).map_err(|e| e.to_string())?;
255 Ok(RulesResult::Updated)
256}
257
258fn write_dedicated(path: &std::path::Path, content: &'static str) -> Result<RulesResult, String> {
259 let is_update = path.exists() && {
260 let existing = std::fs::read_to_string(path).unwrap_or_default();
261 existing.contains(MARKER)
262 };
263
264 std::fs::write(path, content).map_err(|e| e.to_string())?;
265
266 if is_update {
267 Ok(RulesResult::Updated)
268 } else {
269 Ok(RulesResult::Injected)
270 }
271}
272
273fn is_tool_detected(target: &RulesTarget, home: &std::path::Path) -> bool {
278 match target.name {
279 "Claude Code" => {
280 if command_exists("claude") {
281 return true;
282 }
283 home.join(".claude.json").exists() || home.join(".claude").exists()
284 }
285 "Codex CLI" => home.join(".codex").exists() || command_exists("codex"),
286 "Cursor" => home.join(".cursor").exists(),
287 "Windsurf" => home.join(".codeium/windsurf").exists(),
288 "Gemini CLI" => home.join(".gemini").exists(),
289 "VS Code / Copilot" => detect_vscode_installed(home),
290 "Zed" => home.join(".config/zed").exists(),
291 "Cline" => detect_extension_installed(home, "saoudrizwan.claude-dev"),
292 "Roo Code" => detect_extension_installed(home, "rooveterinaryinc.roo-cline"),
293 "OpenCode" => home.join(".config/opencode").exists(),
294 "Continue" => detect_extension_installed(home, "continue.continue"),
295 "Aider" => command_exists("aider") || home.join(".aider.conf.yml").exists(),
296 "Amp" => command_exists("amp") || home.join(".ampcoder").exists(),
297 "Qwen Code" => home.join(".qwen").exists(),
298 "Trae" => home.join(".trae").exists(),
299 "Amazon Q Developer" => home.join(".aws/amazonq").exists(),
300 "JetBrains IDEs" => detect_jetbrains_installed(home),
301 "Antigravity" => home.join(".gemini/antigravity").exists(),
302 "Pi Coding Agent" => home.join(".pi").exists() || command_exists("pi"),
303 _ => false,
304 }
305}
306
307fn command_exists(name: &str) -> bool {
308 #[cfg(target_os = "windows")]
309 let result = std::process::Command::new("where")
310 .arg(name)
311 .output()
312 .map(|o| o.status.success())
313 .unwrap_or(false);
314
315 #[cfg(not(target_os = "windows"))]
316 let result = std::process::Command::new("which")
317 .arg(name)
318 .output()
319 .map(|o| o.status.success())
320 .unwrap_or(false);
321
322 result
323}
324
325fn detect_vscode_installed(home: &std::path::Path) -> bool {
326 let check_dir = |dir: PathBuf| -> bool {
327 dir.join("settings.json").exists() || dir.join("mcp.json").exists()
328 };
329
330 #[cfg(target_os = "macos")]
331 if check_dir(home.join("Library/Application Support/Code/User")) {
332 return true;
333 }
334 #[cfg(target_os = "linux")]
335 if check_dir(home.join(".config/Code/User")) {
336 return true;
337 }
338 #[cfg(target_os = "windows")]
339 if let Ok(appdata) = std::env::var("APPDATA") {
340 if check_dir(PathBuf::from(&appdata).join("Code/User")) {
341 return true;
342 }
343 }
344 false
345}
346
347fn detect_jetbrains_installed(home: &std::path::Path) -> bool {
348 #[cfg(target_os = "macos")]
349 if home.join("Library/Application Support/JetBrains").exists() {
350 return true;
351 }
352 #[cfg(target_os = "linux")]
353 if home.join(".config/JetBrains").exists() {
354 return true;
355 }
356 home.join(".jb-mcp.json").exists()
357}
358
359fn detect_extension_installed(home: &std::path::Path, extension_id: &str) -> bool {
360 #[cfg(target_os = "macos")]
361 {
362 if home
363 .join(format!(
364 "Library/Application Support/Code/User/globalStorage/{extension_id}"
365 ))
366 .exists()
367 {
368 return true;
369 }
370 }
371 #[cfg(target_os = "linux")]
372 {
373 if home
374 .join(format!(".config/Code/User/globalStorage/{extension_id}"))
375 .exists()
376 {
377 return true;
378 }
379 }
380 #[cfg(target_os = "windows")]
381 {
382 if let Ok(appdata) = std::env::var("APPDATA") {
383 if std::path::PathBuf::from(&appdata)
384 .join(format!("Code/User/globalStorage/{extension_id}"))
385 .exists()
386 {
387 return true;
388 }
389 }
390 }
391 false
392}
393
394fn build_rules_targets(home: &std::path::Path) -> Vec<RulesTarget> {
399 vec![
400 RulesTarget {
402 name: "Claude Code",
403 path: home.join(".claude/CLAUDE.md"),
404 format: RulesFormat::SharedMarkdown,
405 },
406 RulesTarget {
407 name: "Codex CLI",
408 path: home.join(".codex/instructions.md"),
409 format: RulesFormat::SharedMarkdown,
410 },
411 RulesTarget {
412 name: "Gemini CLI",
413 path: home.join(".gemini/GEMINI.md"),
414 format: RulesFormat::SharedMarkdown,
415 },
416 RulesTarget {
417 name: "VS Code / Copilot",
418 path: copilot_instructions_path(home),
419 format: RulesFormat::SharedMarkdown,
420 },
421 RulesTarget {
423 name: "Cursor",
424 path: home.join(".cursor/rules/lean-ctx.mdc"),
425 format: RulesFormat::CursorMdc,
426 },
427 RulesTarget {
428 name: "Windsurf",
429 path: home.join(".codeium/windsurf/rules/lean-ctx.md"),
430 format: RulesFormat::DedicatedMarkdown,
431 },
432 RulesTarget {
433 name: "Zed",
434 path: home.join(".config/zed/rules/lean-ctx.md"),
435 format: RulesFormat::DedicatedMarkdown,
436 },
437 RulesTarget {
438 name: "Cline",
439 path: home.join(".cline/rules/lean-ctx.md"),
440 format: RulesFormat::DedicatedMarkdown,
441 },
442 RulesTarget {
443 name: "Roo Code",
444 path: home.join(".roo/rules/lean-ctx.md"),
445 format: RulesFormat::DedicatedMarkdown,
446 },
447 RulesTarget {
448 name: "OpenCode",
449 path: home.join(".config/opencode/rules/lean-ctx.md"),
450 format: RulesFormat::DedicatedMarkdown,
451 },
452 RulesTarget {
453 name: "Continue",
454 path: home.join(".continue/rules/lean-ctx.md"),
455 format: RulesFormat::DedicatedMarkdown,
456 },
457 RulesTarget {
458 name: "Aider",
459 path: home.join(".aider/rules/lean-ctx.md"),
460 format: RulesFormat::DedicatedMarkdown,
461 },
462 RulesTarget {
463 name: "Amp",
464 path: home.join(".ampcoder/rules/lean-ctx.md"),
465 format: RulesFormat::DedicatedMarkdown,
466 },
467 RulesTarget {
468 name: "Qwen Code",
469 path: home.join(".qwen/rules/lean-ctx.md"),
470 format: RulesFormat::DedicatedMarkdown,
471 },
472 RulesTarget {
473 name: "Trae",
474 path: home.join(".trae/rules/lean-ctx.md"),
475 format: RulesFormat::DedicatedMarkdown,
476 },
477 RulesTarget {
478 name: "Amazon Q Developer",
479 path: home.join(".aws/amazonq/rules/lean-ctx.md"),
480 format: RulesFormat::DedicatedMarkdown,
481 },
482 RulesTarget {
483 name: "JetBrains IDEs",
484 path: home.join(".jb-rules/lean-ctx.md"),
485 format: RulesFormat::DedicatedMarkdown,
486 },
487 RulesTarget {
488 name: "Antigravity",
489 path: home.join(".gemini/antigravity/rules/lean-ctx.md"),
490 format: RulesFormat::DedicatedMarkdown,
491 },
492 RulesTarget {
493 name: "Pi Coding Agent",
494 path: home.join(".pi/rules/lean-ctx.md"),
495 format: RulesFormat::DedicatedMarkdown,
496 },
497 ]
498}
499
500fn copilot_instructions_path(home: &std::path::Path) -> PathBuf {
501 #[cfg(target_os = "macos")]
502 {
503 return home.join("Library/Application Support/Code/User/github-copilot-instructions.md");
504 }
505 #[cfg(target_os = "linux")]
506 {
507 return home.join(".config/Code/User/github-copilot-instructions.md");
508 }
509 #[cfg(target_os = "windows")]
510 {
511 if let Ok(appdata) = std::env::var("APPDATA") {
512 return PathBuf::from(appdata).join("Code/User/github-copilot-instructions.md");
513 }
514 }
515 #[allow(unreachable_code)]
516 home.join(".config/Code/User/github-copilot-instructions.md")
517}
518
519#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn shared_rules_have_markers() {
529 assert!(RULES_SHARED.contains(MARKER));
530 assert!(RULES_SHARED.contains(END_MARKER));
531 assert!(RULES_SHARED.contains(RULES_VERSION));
532 }
533
534 #[test]
535 fn dedicated_rules_have_markers() {
536 assert!(RULES_DEDICATED.contains(MARKER));
537 assert!(RULES_DEDICATED.contains(END_MARKER));
538 assert!(RULES_DEDICATED.contains(RULES_VERSION));
539 }
540
541 #[test]
542 fn cursor_mdc_has_markers_and_frontmatter() {
543 assert!(RULES_CURSOR_MDC.contains("lean-ctx"));
544 assert!(RULES_CURSOR_MDC.contains(END_MARKER));
545 assert!(RULES_CURSOR_MDC.contains(RULES_VERSION));
546 assert!(RULES_CURSOR_MDC.contains("alwaysApply: true"));
547 }
548
549 #[test]
550 fn shared_rules_contain_tool_mapping() {
551 assert!(RULES_SHARED.contains("ctx_read"));
552 assert!(RULES_SHARED.contains("ctx_shell"));
553 assert!(RULES_SHARED.contains("ctx_search"));
554 assert!(RULES_SHARED.contains("ctx_tree"));
555 assert!(RULES_SHARED.contains("Write"));
556 }
557
558 #[test]
559 fn shared_rules_litm_optimized() {
560 let lines: Vec<&str> = RULES_SHARED.lines().collect();
561 let first_5 = lines[..5.min(lines.len())].join("\n");
562 assert!(
563 first_5.contains("MANDATORY") || first_5.contains("FORBIDDEN"),
564 "LITM: critical instruction must be near start"
565 );
566 let last_5 = lines[lines.len().saturating_sub(5)..].join("\n");
567 assert!(
568 last_5.contains("REMINDER") || last_5.contains("NEVER"),
569 "LITM: reminder must be near end"
570 );
571 }
572
573 #[test]
574 fn dedicated_rules_contain_modes() {
575 assert!(RULES_DEDICATED.contains("full"));
576 assert!(RULES_DEDICATED.contains("map"));
577 assert!(RULES_DEDICATED.contains("signatures"));
578 assert!(RULES_DEDICATED.contains("diff"));
579 assert!(RULES_DEDICATED.contains("ctx_read"));
580 }
581
582 #[test]
583 fn dedicated_rules_litm_optimized() {
584 let lines: Vec<&str> = RULES_DEDICATED.lines().collect();
585 let first_5 = lines[..5.min(lines.len())].join("\n");
586 assert!(
587 first_5.contains("MANDATORY") || first_5.contains("FORBIDDEN"),
588 "LITM: critical instruction must be near start"
589 );
590 let last_5 = lines[lines.len().saturating_sub(5)..].join("\n");
591 assert!(
592 last_5.contains("REMINDER") || last_5.contains("NEVER"),
593 "LITM: reminder must be near end"
594 );
595 }
596
597 #[test]
598 fn cursor_mdc_litm_optimized() {
599 let lines: Vec<&str> = RULES_CURSOR_MDC.lines().collect();
600 let first_10 = lines[..10.min(lines.len())].join("\n");
601 assert!(
602 first_10.contains("CRITICAL") || first_10.contains("NEVER"),
603 "LITM: critical instruction must be near start of MDC"
604 );
605 let last_5 = lines[lines.len().saturating_sub(5)..].join("\n");
606 assert!(
607 last_5.contains("REMINDER") || last_5.contains("NEVER"),
608 "LITM: reminder must be near end of MDC"
609 );
610 }
611
612 fn ensure_temp_dir() {
613 let tmp = std::env::temp_dir();
614 if !tmp.exists() {
615 std::fs::create_dir_all(&tmp).ok();
616 }
617 }
618
619 #[test]
620 fn replace_section_with_end_marker() {
621 ensure_temp_dir();
622 let old = "user stuff\n\n# lean-ctx — Context Engineering Layer\n<!-- lean-ctx-rules-v2 -->\nold rules\n<!-- /lean-ctx -->\nmore user stuff\n";
623 let path = std::env::temp_dir().join("test_replace_with_end.md");
624 std::fs::write(&path, old).unwrap();
625
626 let result = replace_markdown_section(&path, old).unwrap();
627 assert!(matches!(result, RulesResult::Updated));
628
629 let new_content = std::fs::read_to_string(&path).unwrap();
630 assert!(new_content.contains(RULES_VERSION));
631 assert!(new_content.starts_with("user stuff"));
632 assert!(new_content.contains("more user stuff"));
633 assert!(!new_content.contains("lean-ctx-rules-v2"));
634
635 std::fs::remove_file(&path).ok();
636 }
637
638 #[test]
639 fn replace_section_without_end_marker() {
640 ensure_temp_dir();
641 let old = "user stuff\n\n# lean-ctx — Context Engineering Layer\nold rules only\n";
642 let path = std::env::temp_dir().join("test_replace_no_end.md");
643 std::fs::write(&path, old).unwrap();
644
645 let result = replace_markdown_section(&path, old).unwrap();
646 assert!(matches!(result, RulesResult::Updated));
647
648 let new_content = std::fs::read_to_string(&path).unwrap();
649 assert!(new_content.contains(RULES_VERSION));
650 assert!(new_content.starts_with("user stuff"));
651
652 std::fs::remove_file(&path).ok();
653 }
654
655 #[test]
656 fn append_to_shared_preserves_existing() {
657 ensure_temp_dir();
658 let path = std::env::temp_dir().join("test_append_shared.md");
659 std::fs::write(&path, "existing user rules\n").unwrap();
660
661 let result = append_to_shared(&path).unwrap();
662 assert!(matches!(result, RulesResult::Injected));
663
664 let content = std::fs::read_to_string(&path).unwrap();
665 assert!(content.starts_with("existing user rules"));
666 assert!(content.contains(MARKER));
667 assert!(content.contains(END_MARKER));
668
669 std::fs::remove_file(&path).ok();
670 }
671
672 #[test]
673 fn write_dedicated_creates_file() {
674 ensure_temp_dir();
675 let path = std::env::temp_dir().join("test_write_dedicated.md");
676 if path.exists() {
677 std::fs::remove_file(&path).ok();
678 }
679
680 let result = write_dedicated(&path, RULES_DEDICATED).unwrap();
681 assert!(matches!(result, RulesResult::Injected));
682
683 let content = std::fs::read_to_string(&path).unwrap();
684 assert!(content.contains(MARKER));
685 assert!(content.contains("ctx_read modes"));
686
687 std::fs::remove_file(&path).ok();
688 }
689
690 #[test]
691 fn write_dedicated_updates_existing() {
692 ensure_temp_dir();
693 let path = std::env::temp_dir().join("test_write_dedicated_update.md");
694 std::fs::write(&path, "# lean-ctx — Context Engineering Layer\nold version").unwrap();
695
696 let result = write_dedicated(&path, RULES_DEDICATED).unwrap();
697 assert!(matches!(result, RulesResult::Updated));
698
699 std::fs::remove_file(&path).ok();
700 }
701
702 #[test]
703 fn target_count() {
704 let home = std::path::PathBuf::from("/tmp/fake_home");
705 let targets = build_rules_targets(&home);
706 assert_eq!(targets.len(), 19);
707 }
708}